Convert POP3 to service

* Remove MessagingController and (almost all of) MailService

Change-Id: I8953b58b237de6a71fda770f1727bd94081fec55
This commit is contained in:
Marc Blank 2012-02-14 10:55:45 -08:00
parent d57e096b2c
commit 4f813fb129
22 changed files with 1577 additions and 3490 deletions

View File

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

View File

@ -444,6 +444,23 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService {
return (Integer)mReturn;
}
}
/**
* Request the service to send mail in the specified account's Outbox
*
* @param accountId the account whose outgoing mail should be sent
*/
@Override
public void sendMail(final long accountId) throws RemoteException {
setTask(new ProxyTask() {
@Override
public void run() throws RemoteException{
if (mCallback != null) mService.setCallback(mCallback);
mService.sendMail(accountId);
}
}, "sendMail");
}
@Override
public IBinder asBinder() {
return null;

View File

@ -57,4 +57,6 @@ interface IEmailService {
// API level 2
int searchMessages(long accountId, in SearchParams params, long destMailboxId);
void sendMail(long accountId);
}

View File

@ -16,33 +16,26 @@
package com.android.email;
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.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;
import com.android.email.mail.store.Pop3Store.Pop3Message;
import com.android.email.provider.AccountBackupRestore;
import com.android.email.provider.Utilities;
import com.android.email.service.EmailServiceUtils;
import com.android.email.service.MailService;
import com.android.emailcommon.Api;
import com.android.emailcommon.Logging;
import com.android.emailcommon.mail.AuthenticationFailedException;
import com.android.emailcommon.mail.Folder.MessageRetrievalListener;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.Attachment;
import com.android.emailcommon.provider.EmailContent.Body;
import com.android.emailcommon.provider.EmailContent.MailboxColumns;
import com.android.emailcommon.provider.EmailContent.Message;
import com.android.emailcommon.provider.EmailContent.MessageColumns;
@ -74,12 +67,9 @@ import java.util.concurrent.ConcurrentHashMap;
* to IMAP, POP3, and EAS by AttachmentDownloadService
*/
public class Controller {
private static final String TAG = "Controller";
private static Controller sInstance;
private final Context mContext;
private Context mProviderContext;
private final MessagingController mLegacyController;
private final LegacyListener mLegacyListener = new LegacyListener();
private final ServiceCallback mServiceCallback = new ServiceCallback();
private final HashSet<Result> mListeners = new HashSet<Result>();
/*package*/ final ConcurrentHashMap<Long, Boolean> mLegacyControllerMap =
@ -102,11 +92,6 @@ public class Controller {
};
private static final int MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID = 1;
private static final String[] BODY_SOURCE_KEY_PROJECTION =
new String[] {Body.SOURCE_MESSAGE_KEY};
private static final int BODY_SOURCE_KEY_COLUMN = 0;
private static final String WHERE_MESSAGE_KEY = Body.MESSAGE_KEY + "=?";
private static final String MAILBOXES_FOR_ACCOUNT_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?";
private static final String MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION =
MAILBOXES_FOR_ACCOUNT_SELECTION + " AND " + MailboxColumns.TYPE + "!=" +
@ -122,8 +107,6 @@ public class Controller {
protected Controller(Context _context) {
mContext = _context.getApplicationContext();
mProviderContext = _context;
mLegacyController = MessagingController.getInstance(mProviderContext, this);
mLegacyController.addListener(mLegacyListener);
}
/**
@ -135,16 +118,6 @@ public class Controller {
mInUnitTests = inUnitTests;
}
/**
* Cleanup for test. Mustn't be called for the regular {@link Controller}, as it's a
* singleton and lives till the process finishes.
*
* <p>However, this method MUST be called for mock instances.
*/
public void cleanupForTest() {
mLegacyController.removeListener(mLegacyListener);
}
/**
* Gets or creates the singleton instance of Controller.
*/
@ -309,8 +282,8 @@ public class Controller {
// Commit the message to the local store
msg.save(mProviderContext);
// Setup the rest of the message and mark it completely loaded
mLegacyController.copyOneMessageToProvider(pop3Message, msg,
Message.FLAG_LOADED_COMPLETE, mProviderContext);
Utilities.copyOneMessageToProvider(mProviderContext, pop3Message, msg,
Message.FLAG_LOADED_COMPLETE);
// Restore the complete message and return it
return Message.restoreMessageWithId(mProviderContext, msg.mId);
} catch (MessagingException e) {
@ -356,35 +329,12 @@ public class Controller {
Log.d("updateMailboxList", "RemoteException" + e);
}
} else {
// MessagingController implementation
mLegacyController.listFolders(accountId, mLegacyListener);
throw new IllegalStateException("No service for updateMailboxList?");
}
}
});
}
/**
* Request a remote update of a mailbox. For use by the timed service.
*
* Functionally this is quite similar to updateMailbox(), but it's a separate API and
* separate callback in order to keep UI callbacks from affecting the service loop.
*/
@SuppressWarnings("deprecation")
public void serviceCheckMail(final long accountId, final long mailboxId, final long tag) {
IEmailService service = getServiceForAccount(accountId);
if (service != null) {
mLegacyListener.checkMailFinished(mContext, accountId, mailboxId, tag);
} else {
// MessagingController implementation
Utility.runAsync(new Runnable() {
@Override
public void run() {
mLegacyController.checkMail(accountId, tag, mLegacyListener);
}
});
}
}
/**
* Request a remote update of a mailbox.
*
@ -392,7 +342,6 @@ public class Controller {
* a simple message list. We should also at this point queue up a background task of
* downloading some/all of the messages in this mailbox, but that should be interruptable.
*/
@SuppressWarnings("deprecation")
public void updateMailbox(final long accountId, final long mailboxId, boolean userRequest) {
IEmailService service = getServiceForAccount(accountId);
@ -404,24 +353,9 @@ public class Controller {
// is implemented for other protocols
Log.d("updateMailbox", "RemoteException" + e);
}
} else {
// MessagingController implementation
Utility.runAsync(new Runnable() {
@Override
public void run() {
// TODO shouldn't be passing fully-build accounts & mailboxes into APIs
Account account =
Account.restoreAccountWithId(mProviderContext, accountId);
Mailbox mailbox =
Mailbox.restoreMailboxWithId(mProviderContext, mailboxId);
if (account == null || mailbox == null ||
mailbox.mType == Mailbox.TYPE_SEARCH) {
return;
}
mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener);
}
});
}
} else {
throw new IllegalStateException("No service for loadMessageForView?");
}
}
/**
@ -434,40 +368,29 @@ public class Controller {
* @param messageId the message to load
* @param callback the Controller callback by which results will be reported
*/
@SuppressWarnings("deprecation")
public void loadMessageForView(final long messageId) {
// Split here for target type (Service or MessagingController)
EmailServiceProxy service = getServiceForMessage(messageId);
if (service != null && service.isRemote()) {
// Get rid of this!!
if (service.isRemote()) {
// There is no service implementation, so we'll just jam the value, log the error,
// and get out of here.
Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, messageId);
ContentValues cv = new ContentValues();
cv.put(MessageColumns.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE);
mProviderContext.getContentResolver().update(uri, cv, null, null);
Log.d(Logging.LOG_TAG, "Unexpected loadMessageForView() for service-based message.");
Log.d(Logging.LOG_TAG, "Unexpected loadMessageForView() for remote service message.");
final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId);
synchronized (mListeners) {
for (Result listener : mListeners) {
listener.loadMessageForViewCallback(null, accountId, messageId, 100);
}
}
} else if (service != null) {
// IMAP here for now
} else {
try {
service.loadMore(messageId);
} catch (RemoteException e) {
}
} else {
// MessagingController implementation
Utility.runAsync(new Runnable() {
@Override
public void run() {
mLegacyController.loadMessageForView(messageId, mLegacyListener);
}
});
}
}
@ -598,50 +521,12 @@ public class Controller {
sendPendingMessages(accountId);
}
@SuppressWarnings("deprecation")
private void sendPendingMessagesSmtp(long accountId) {
// for IMAP & POP only, (attempt to) send the message now
final Account account =
Account.restoreAccountWithId(mProviderContext, accountId);
if (account == null) {
return;
}
final long sentboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_SENT);
Utility.runAsync(new Runnable() {
@Override
public void run() {
mLegacyController.sendPendingMessages(account, sentboxId, mLegacyListener);
}
});
}
/**
* Try to send all pending messages for a given account
*
* @param accountId the account for which to send messages
*/
public void sendPendingMessages(long accountId) {
// 1. make sure we even have an outbox, exit early if not
final long outboxId =
Mailbox.findMailboxOfType(mProviderContext, accountId, Mailbox.TYPE_OUTBOX);
if (outboxId == Mailbox.NO_MAILBOX) {
return;
}
// 2. dispatch as necessary
IEmailService service = getServiceForAccount(accountId);
if (service != null) {
// Service implementation
try {
service.startSync(outboxId, false);
} catch (RemoteException e) {
// TODO Change exception handling to be consistent with however this method
// is implemented for other protocols
Log.d("updateMailbox", "RemoteException" + e);
}
} else {
// MessagingController implementation
sendPendingMessagesSmtp(accountId);
private void sendPendingMessages(long accountId) {
EmailServiceProxy service =
EmailServiceUtils.getServiceForAccount(mContext, null, accountId);
try {
service.sendMail(accountId);
} catch (RemoteException e) {
}
}
@ -718,7 +603,7 @@ public class Controller {
mProviderContext.getContentResolver().update(uri, cv, null, null);
// Trigger a refresh using the new, longer limit
mailbox.mVisibleLimit += Email.VISIBLE_LIMIT_INCREMENT;
mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener);
//mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener);
}
});
}
@ -1005,24 +890,13 @@ public class Controller {
// TODO Change exception handling to be consistent with however this method
// is implemented for other protocols
Log.e("searchMessages", "RemoteException", e);
return 0;
}
} else {
// This is the actual mailbox we'll be searching
Mailbox actualMailbox = Mailbox.restoreMailboxWithId(mContext, searchParams.mMailboxId);
if (actualMailbox == null) {
Log.e(Logging.LOG_TAG, "Unable to find mailbox " + searchParams.mMailboxId
+ " to search in with " + searchParams);
return 0;
}
// Do the search
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, "Search: " + searchParams.mFilter);
}
// Plumb this
return 0;
//return mLegacyController.searchMailbox(accountId, searchParams, searchMailboxId);
}
return 0;
}
private EmailServiceProxy getServiceForAccount(long accountId) {
return EmailServiceUtils.getServiceForAccount(mContext, mCallbackProxy, accountId);
}
/**
@ -1096,46 +970,6 @@ public class Controller {
return getServiceForAccount(message.mAccountKey);
}
/**
* For a given account id, return a service proxy if applicable, or null.
*
* @param accountId the message of interest
* @result service proxy, or null if n/a
*/
private EmailServiceProxy getServiceForAccount(long accountId) {
if (isMessagingController(accountId)) return null;
if (Account.getProtocol(mContext, accountId).equals(HostAuth.SCHEME_IMAP)) {
return getImapEmailService();
}
return getExchangeEmailService();
}
private EmailServiceProxy getExchangeEmailService() {
return EmailServiceUtils.getExchangeService(mContext, mServiceCallback);
}
private EmailServiceProxy getImapEmailService() {
return EmailServiceUtils.getImapService(mContext, mServiceCallback);
}
/**
* Simple helper to determine if legacy MessagingController should be used
*/
public boolean isMessagingController(Account account) {
if (account == null) return false;
return isMessagingController(account.mId);
}
public boolean isMessagingController(long accountId) {
Boolean isLegacyController = mLegacyControllerMap.get(accountId);
if (isLegacyController == null) {
String protocol = Account.getProtocol(mProviderContext, accountId);
isLegacyController = (HostAuth.SCHEME_POP3.equals(protocol));
mLegacyControllerMap.put(accountId, isLegacyController);
}
return isLegacyController;
}
/**
* Delete an account.
*/
@ -1177,7 +1011,6 @@ public class Controller {
SecurityPolicy.getInstance(context).reducePolicies();
Email.setServicesEnabledSync(context);
Email.setNotifyUiAccountsChanged(true);
MailService.actionReschedule(context);
} catch (Exception e) {
Log.w(Logging.LOG_TAG, "Exception while deleting account", e);
}
@ -1343,248 +1176,6 @@ public class Controller {
}
}
/**
* Bridge to intercept {@link MessageRetrievalListener#loadAttachmentProgress} and
* pass down to {@link Result}.
*/
public class MessageRetrievalListenerBridge implements MessageRetrievalListener {
private final long mMessageId;
private final long mAttachmentId;
private final long mAccountId;
public MessageRetrievalListenerBridge(long messageId, long attachmentId) {
mMessageId = messageId;
mAttachmentId = attachmentId;
mAccountId = Account.getAccountIdForMessageId(mProviderContext, mMessageId);
}
@Override
public void loadAttachmentProgress(int progress) {
synchronized (mListeners) {
for (Result listener : mListeners) {
listener.loadAttachmentCallback(null, mAccountId, mMessageId, mAttachmentId,
progress);
}
}
}
@Override
public void messageRetrieved(com.android.emailcommon.mail.Message message) {
}
}
/**
* Support for receiving callbacks from MessagingController and dealing with UI going
* out of scope.
*/
public class LegacyListener extends MessagingListener {
public LegacyListener() {
}
@Override
public void listFoldersStarted(long accountId) {
synchronized (mListeners) {
for (Result l : mListeners) {
l.updateMailboxListCallback(null, accountId, 0);
}
}
}
@Override
public void listFoldersFailed(long accountId, String message) {
synchronized (mListeners) {
for (Result l : mListeners) {
l.updateMailboxListCallback(new MessagingException(message), accountId, 0);
}
}
}
@Override
public void listFoldersFinished(long accountId) {
synchronized (mListeners) {
for (Result l : mListeners) {
l.updateMailboxListCallback(null, accountId, 100);
}
}
}
@Override
public void synchronizeMailboxStarted(long accountId, long mailboxId) {
synchronized (mListeners) {
for (Result l : mListeners) {
l.updateMailboxCallback(null, accountId, mailboxId, 0, 0, null);
}
}
}
@Override
public void synchronizeMailboxFinished(long accountId, long mailboxId,
int totalMessagesInMailbox, int numNewMessages, ArrayList<Long> addedMessages) {
synchronized (mListeners) {
for (Result l : mListeners) {
l.updateMailboxCallback(null, accountId, mailboxId, 100, numNewMessages,
addedMessages);
}
}
}
@Override
public void synchronizeMailboxFailed(long accountId, long mailboxId, Exception e) {
MessagingException me;
if (e instanceof MessagingException) {
me = (MessagingException) e;
} else {
me = new MessagingException(e.toString());
}
synchronized (mListeners) {
for (Result l : mListeners) {
l.updateMailboxCallback(me, accountId, mailboxId, 0, 0, null);
}
}
}
@Override
public void checkMailStarted(Context context, long accountId, long tag) {
synchronized (mListeners) {
for (Result l : mListeners) {
l.serviceCheckMailCallback(null, accountId, -1, 0, tag);
}
}
}
@Override
public void checkMailFinished(Context context, long accountId, long folderId, long tag) {
synchronized (mListeners) {
for (Result l : mListeners) {
l.serviceCheckMailCallback(null, accountId, folderId, 100, tag);
}
}
}
@Override
public void loadMessageForViewStarted(long messageId) {
final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId);
synchronized (mListeners) {
for (Result listener : mListeners) {
listener.loadMessageForViewCallback(null, accountId, messageId, 0);
}
}
}
@Override
public void loadMessageForViewFinished(long messageId) {
final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId);
synchronized (mListeners) {
for (Result listener : mListeners) {
listener.loadMessageForViewCallback(null, accountId, messageId, 100);
}
}
}
@Override
public void loadMessageForViewFailed(long messageId, String message) {
final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId);
synchronized (mListeners) {
for (Result listener : mListeners) {
listener.loadMessageForViewCallback(new MessagingException(message),
accountId, messageId, 0);
}
}
}
@Override
public void loadAttachmentStarted(long accountId, long messageId, long attachmentId,
boolean requiresDownload) {
try {
mCallbackProxy.loadAttachmentStatus(messageId, attachmentId,
EmailServiceStatus.IN_PROGRESS, 0);
} catch (RemoteException e) {
}
synchronized (mListeners) {
for (Result listener : mListeners) {
listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 0);
}
}
}
@Override
public void loadAttachmentFinished(long accountId, long messageId, long attachmentId) {
try {
mCallbackProxy.loadAttachmentStatus(messageId, attachmentId,
EmailServiceStatus.SUCCESS, 100);
} catch (RemoteException e) {
}
synchronized (mListeners) {
for (Result listener : mListeners) {
listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 100);
}
}
}
@Override
public void loadAttachmentFailed(long accountId, long messageId, long attachmentId,
MessagingException me, boolean background) {
try {
// If the cause of the MessagingException is an IOException, we send a status of
// CONNECTION_ERROR; in this case, AttachmentDownloadService will try again to
// download the attachment. Otherwise, the error is considered non-recoverable.
int status = EmailServiceStatus.ATTACHMENT_NOT_FOUND;
if (me != null && me.getCause() instanceof IOException) {
status = EmailServiceStatus.CONNECTION_ERROR;
}
mCallbackProxy.loadAttachmentStatus(messageId, attachmentId, status, 0);
} catch (RemoteException e) {
}
synchronized (mListeners) {
for (Result listener : mListeners) {
// TODO We are overloading the exception here. The UI listens for this
// callback and displays a toast if the exception is not null. Since we
// want to avoid displaying toast for background operations, we force
// the exception to be null. This needs to be re-worked so the UI will
// only receive (or at least pays attention to) responses for requests
// it explicitly cares about. Then we would not need to overload the
// exception parameter.
listener.loadAttachmentCallback(background ? null : me, accountId, messageId,
attachmentId, 0);
}
}
}
@Override
synchronized public void sendPendingMessagesStarted(long accountId, long messageId) {
synchronized (mListeners) {
for (Result listener : mListeners) {
listener.sendMailCallback(null, accountId, messageId, 0);
}
}
}
@Override
synchronized public void sendPendingMessagesCompleted(long accountId) {
synchronized (mListeners) {
for (Result listener : mListeners) {
listener.sendMailCallback(null, accountId, -1, 100);
}
}
}
@Override
synchronized public void sendPendingMessagesFailed(long accountId, long messageId,
Exception reason) {
MessagingException me;
if (reason instanceof MessagingException) {
me = (MessagingException) reason;
} else {
me = new MessagingException(reason.toString());
}
synchronized (mListeners) {
for (Result listener : mListeners) {
listener.sendMailCallback(me, accountId, messageId, 0);
}
}
}
}
/**
* Service callback for service operations
*/
@ -1764,8 +1355,7 @@ public class Controller {
* Proxy that can be used to broadcast service callbacks; we currently use this only for
* loadAttachment callbacks
*/
private final IEmailServiceCallback.Stub mCallbackProxy =
new IEmailServiceCallback.Stub() {
private final IEmailServiceCallback.Stub mCallbackProxy = new IEmailServiceCallback.Stub() {
/**
* Broadcast a callback to the everyone that's registered
@ -1799,147 +1389,23 @@ public class Controller {
}
@Override
public void sendMessageStatus(long messageId, String subject, int statusCode, int progress){
public void syncMailboxListStatus(long accountId, int statusCode, int progress)
throws RemoteException {
}
@Override
public void loadMessageStatus(long messageId, int statusCode, int progress){
public void syncMailboxStatus(long mailboxId, int statusCode, int progress)
throws RemoteException {
}
@Override
public void syncMailboxListStatus(long accountId, int statusCode, int progress) {
public void sendMessageStatus(long messageId, String subject, int statusCode, int progress)
throws RemoteException {
}
@Override
public void syncMailboxStatus(long mailboxId, int statusCode, int progress) {
public void loadMessageStatus(long messageId, int statusCode, int progress)
throws RemoteException {
}
};
public static class ControllerService extends Service {
/**
* Create our EmailService implementation here. For now, only loadAttachment is supported;
* the intention, however, is to move more functionality to the service interface
*/
private final IEmailService.Stub mBinder = new IEmailService.Stub() {
@Override
public Bundle validate(HostAuth hostAuth) {
return null;
}
@Override
public Bundle autoDiscover(String userName, String password) {
return null;
}
@Override
public void startSync(long mailboxId, boolean userRequest) {
}
@Override
public void stopSync(long mailboxId) {
}
@Override
public void loadAttachment(long attachmentId, boolean background)
throws RemoteException {
Attachment att = Attachment.restoreAttachmentWithId(ControllerService.this,
attachmentId);
if (att != null) {
if (Email.DEBUG) {
Log.d(TAG, "loadAttachment " + attachmentId + ": " + att.mFileName);
}
Message msg = Message.restoreMessageWithId(ControllerService.this,
att.mMessageKey);
if (msg != null) {
// If the message is a forward and the attachment needs downloading, we need
// to retrieve the message from the source, rather than from the message
// itself
if ((msg.mFlags & Message.FLAG_TYPE_FORWARD) != 0) {
String[] cols = Utility.getRowColumns(ControllerService.this,
Body.CONTENT_URI, BODY_SOURCE_KEY_PROJECTION, WHERE_MESSAGE_KEY,
new String[] {Long.toString(msg.mId)});
if (cols != null) {
msg = Message.restoreMessageWithId(ControllerService.this,
Long.parseLong(cols[BODY_SOURCE_KEY_COLUMN]));
if (msg == null) {
// TODO: We can try restoring from the deleted table here...
return;
}
}
}
MessagingController legacyController = sInstance.mLegacyController;
LegacyListener legacyListener = sInstance.mLegacyListener;
legacyController.loadAttachment(msg.mAccountKey, msg.mId, msg.mMailboxKey,
attachmentId, legacyListener, background);
} else {
// Send back the specific error status for this case
sInstance.mCallbackProxy.loadAttachmentStatus(att.mMessageKey, attachmentId,
EmailServiceStatus.MESSAGE_NOT_FOUND, 0);
}
}
}
@Override
public void updateFolderList(long accountId) {
}
@Override
public void hostChanged(long accountId) {
}
@Override
public void setLogging(int flags) {
}
@Override
public void sendMeetingResponse(long messageId, int response) {
}
@Override
public void loadMore(long messageId) {
}
// The following three methods are not implemented in this version
@Override
public boolean createFolder(long accountId, String name) {
return false;
}
@Override
public boolean deleteFolder(long accountId, String name) {
return false;
}
@Override
public boolean renameFolder(long accountId, String oldName, String newName) {
return false;
}
@Override
public void setCallback(IEmailServiceCallback cb) {
sCallbackList.register(cb);
}
@Override
public void deleteAccountPIMData(long accountId) {
}
@Override
public int searchMessages(long accountId, SearchParams searchParams,
long destMailboxId) {
return 0;
}
@Override
public int getApiLevel() {
return Api.LEVEL;
}
};
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}
}

View File

@ -28,7 +28,6 @@ import com.android.email.activity.MessageCompose;
import com.android.email.activity.ShortcutPicker;
import com.android.email.service.AttachmentDownloadService;
import com.android.email.service.MailService;
import com.android.email.widget.WidgetConfiguration;
import com.android.emailcommon.Logging;
import com.android.emailcommon.TempDirectory;
import com.android.emailcommon.provider.Account;
@ -127,15 +126,6 @@ public class Email extends Application {
private static void setServicesEnabled(Context context, boolean enabled) {
PackageManager pm = context.getPackageManager();
if (!enabled && pm.getComponentEnabledSetting(
new ComponentName(context, MailService.class)) ==
PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
/*
* If no accounts now exist but the service is still enabled we're about to disable it
* so we'll reschedule to kill off any existing alarms.
*/
MailService.actionReschedule(context);
}
pm.setComponentEnabledSetting(
new ComponentName(context, MessageCompose.class),
enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
@ -156,15 +146,6 @@ public class Email extends Application {
enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
if (enabled && pm.getComponentEnabledSetting(
new ComponentName(context, MailService.class)) ==
PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
/*
* And now if accounts do exist then we've just enabled the service and we want to
* schedule alarms for the new accounts.
*/
MailService.actionReschedule(context);
}
// Start/stop the various services depending on whether there are any accounts
startOrStopService(enabled, context, new Intent(context, AttachmentDownloadService.class));

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,6 @@ import com.android.email.Preferences;
import com.android.email.R;
import com.android.email.activity.UiUtilities;
import com.android.email.service.EmailServiceUtils;
import com.android.email.service.MailService;
import com.android.emailcommon.Logging;
import android.app.Fragment;
@ -44,7 +43,6 @@ public class DebugFragment extends Fragment implements OnCheckedChangeListener,
private CheckBox mEnableExchangeLoggingView;
private CheckBox mEnableExchangeFileLoggingView;
private CheckBox mInhibitGraphicsAccelerationView;
private CheckBox mForceOneMinuteRefreshView;
private CheckBox mEnableStrictModeView;
private Preferences mPreferences;
@ -92,11 +90,6 @@ public class DebugFragment extends Fragment implements OnCheckedChangeListener,
mInhibitGraphicsAccelerationView.setChecked(Email.sDebugInhibitGraphicsAcceleration);
mInhibitGraphicsAccelerationView.setOnCheckedChangeListener(this);
mForceOneMinuteRefreshView = (CheckBox)
UiUtilities.getView(view, R.id.debug_force_one_minute_refresh);
mForceOneMinuteRefreshView.setChecked(mPreferences.getForceOneMinuteRefresh());
mForceOneMinuteRefreshView.setOnCheckedChangeListener(this);
mEnableStrictModeView = (CheckBox)
UiUtilities.getView(view, R.id.debug_enable_strict_mode);
mEnableStrictModeView.setChecked(mPreferences.getEnableStrictMode());
@ -125,10 +118,6 @@ public class DebugFragment extends Fragment implements OnCheckedChangeListener,
Email.sDebugInhibitGraphicsAcceleration = isChecked;
mPreferences.setInhibitGraphicsAcceleration(isChecked);
break;
case R.id.debug_force_one_minute_refresh:
mPreferences.setForceOneMinuteRefresh(isChecked);
MailService.actionReschedule(getActivity());
break;
case R.id.debug_enable_strict_mode:
mPreferences.setEnableStrictMode(isChecked);
Email.enableStrictMode(isChecked);

View File

@ -0,0 +1,150 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.email.provider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
import com.android.email.LegacyConversions;
import com.android.emailcommon.Logging;
import com.android.emailcommon.internet.MimeUtility;
import com.android.emailcommon.mail.Message;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.mail.Part;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.MessageColumns;
import com.android.emailcommon.provider.EmailContent.SyncColumns;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.utility.ConversionUtilities;
import java.io.IOException;
import java.util.ArrayList;
public class Utilities {
/**
* Copy one downloaded message (which may have partially-loaded sections)
* into a newly created EmailProvider Message, given the account and mailbox
*
* @param message the remote message we've just downloaded
* @param account the account it will be stored into
* @param folder the mailbox it will be stored into
* @param loadStatus when complete, the message will be marked with this status (e.g.
* EmailContent.Message.LOADED)
*/
public static void copyOneMessageToProvider(Context context, Message message, Account account,
Mailbox folder, int loadStatus) {
EmailContent.Message localMessage = null;
Cursor c = null;
try {
c = context.getContentResolver().query(
EmailContent.Message.CONTENT_URI,
EmailContent.Message.CONTENT_PROJECTION,
EmailContent.MessageColumns.ACCOUNT_KEY + "=?" +
" AND " + MessageColumns.MAILBOX_KEY + "=?" +
" AND " + SyncColumns.SERVER_ID + "=?",
new String[] {
String.valueOf(account.mId),
String.valueOf(folder.mId),
String.valueOf(message.getUid())
},
null);
if (c.moveToNext()) {
localMessage = EmailContent.getContent(c, EmailContent.Message.class);
localMessage.mMailboxKey = folder.mId;
localMessage.mAccountKey = account.mId;
copyOneMessageToProvider(context, message, localMessage, loadStatus);
}
} finally {
if (c != null) {
c.close();
}
}
}
/**
* Copy one downloaded message (which may have partially-loaded sections)
* into an already-created EmailProvider Message
*
* @param message the remote message we've just downloaded
* @param localMessage the EmailProvider Message, already created
* @param loadStatus when complete, the message will be marked with this status (e.g.
* EmailContent.Message.LOADED)
* @param context the context to be used for EmailProvider
*/
public static void copyOneMessageToProvider(Context context, Message message,
EmailContent.Message localMessage, int loadStatus) {
try {
EmailContent.Body body = EmailContent.Body.restoreBodyWithMessageId(context,
localMessage.mId);
if (body == null) {
body = new EmailContent.Body();
}
try {
// Copy the fields that are available into the message object
LegacyConversions.updateMessageFields(localMessage, message,
localMessage.mAccountKey, localMessage.mMailboxKey);
// Now process body parts & attachments
ArrayList<Part> viewables = new ArrayList<Part>();
ArrayList<Part> attachments = new ArrayList<Part>();
MimeUtility.collectParts(message, viewables, attachments);
ConversionUtilities.updateBodyFields(body, localMessage, viewables);
// Commit the message & body to the local store immediately
saveOrUpdate(localMessage, context);
saveOrUpdate(body, context);
// process (and save) attachments
LegacyConversions.updateAttachments(context, localMessage, attachments);
// One last update of message with two updated flags
localMessage.mFlagLoaded = loadStatus;
ContentValues cv = new ContentValues();
cv.put(EmailContent.MessageColumns.FLAG_ATTACHMENT, localMessage.mFlagAttachment);
cv.put(EmailContent.MessageColumns.FLAG_LOADED, localMessage.mFlagLoaded);
Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI,
localMessage.mId);
context.getContentResolver().update(uri, cv, null, null);
} catch (MessagingException me) {
Log.e(Logging.LOG_TAG, "Error while copying downloaded message." + me);
}
} catch (RuntimeException rte) {
Log.e(Logging.LOG_TAG, "Error while storing downloaded message." + rte.toString());
} catch (IOException ioe) {
Log.e(Logging.LOG_TAG, "Error while storing attachment." + ioe.toString());
}
}
public static void saveOrUpdate(EmailContent content, Context context) {
if (content.isSaved()) {
content.update(context, content.toContentValues());
} else {
content.save(context);
}
}
}

View File

@ -33,7 +33,6 @@ import android.text.format.DateUtils;
import android.util.Log;
import com.android.email.AttachmentInfo;
import com.android.email.Controller.ControllerService;
import com.android.email.Email;
import com.android.email.EmailConnectivityManager;
import com.android.email.NotificationController;
@ -450,8 +449,8 @@ public class AttachmentDownloadService extends Service implements Runnable {
* @return whether or not the download was started
*/
/*package*/ synchronized boolean tryStartDownload(DownloadRequest req) {
Intent intent = getServiceIntentForAccount(req.accountId);
if (intent == null) return false;
EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(
AttachmentDownloadService.this, mServiceCallback, req.accountId);
// Do not download the same attachment multiple times
boolean alreadyInProgress = mDownloadsInProgress.get(req.attachmentId) != null;
@ -461,7 +460,7 @@ public class AttachmentDownloadService extends Service implements Runnable {
if (Email.DEBUG) {
Log.d(TAG, ">> Starting download for attachment #" + req.attachmentId);
}
startDownload(intent, req);
startDownload(service, req);
} catch (RemoteException e) {
// TODO: Consider whether we need to do more in this case...
// For now, fix up our data to reflect the failure
@ -478,18 +477,16 @@ public class AttachmentDownloadService extends Service implements Runnable {
* Do the work of starting an attachment download using the EmailService interface, and
* set our watchdog alarm
*
* @param serviceClass the class that will attempt the download
* @param serviceClass the service handling the download
* @param req the DownloadRequest
* @throws RemoteException
*/
private void startDownload(Intent intent, DownloadRequest req)
private void startDownload(EmailServiceProxy service, DownloadRequest req)
throws RemoteException {
req.startTime = System.currentTimeMillis();
req.inProgress = true;
mDownloadsInProgress.put(req.attachmentId, req);
EmailServiceProxy proxy =
new EmailServiceProxy(mContext, intent, mServiceCallback);
proxy.loadAttachment(req.attachmentId, req.priority != PRIORITY_FOREGROUND);
service.loadAttachment(req.attachmentId, req.priority != PRIORITY_FOREGROUND);
// Lazily initialize our (reusable) pending intent
if (mWatchdogPendingIntent == null) {
createWatchdogPendingIntent(mContext);
@ -685,11 +682,6 @@ public class AttachmentDownloadService extends Service implements Runnable {
}
}
@Override
public void sendMessageStatus(long messageId, String subject, int statusCode, int progress)
throws RemoteException {
}
@Override
public void syncMailboxListStatus(long accountId, int statusCode, int progress)
throws RemoteException {
@ -700,33 +692,17 @@ public class AttachmentDownloadService extends Service implements Runnable {
throws RemoteException {
}
@Override
public void sendMessageStatus(long messageId, String subject, int statusCode, int progress)
throws RemoteException {
}
@Override
public void loadMessageStatus(long messageId, int statusCode, int progress)
throws RemoteException {
}
}
/**
* Return an Intent to be used used based on the account type of the provided account id. We
* cache the results to avoid repeated database access
* @param accountId the id of the account
* @return the Intent to be used for the account or null (if the account no longer exists)
*/
private synchronized Intent getServiceIntentForAccount(long accountId) {
// TODO: We should have some more data-driven way of determining the service intent.
Intent serviceIntent = mAccountServiceMap.get(accountId);
if (serviceIntent == null) {
String protocol = Account.getProtocol(mContext, accountId);
if (protocol == null) return null;
serviceIntent = new Intent(mContext, ControllerService.class);
if (protocol.equals("eas")) {
serviceIntent = new Intent(EmailServiceProxy.EXCHANGE_INTENT);
}
mAccountServiceMap.put(accountId, serviceIntent);
}
return serviceIntent;
}
/*package*/ void addServiceIntentForTest(long accountId, Intent intent) {
mAccountServiceMap.put(accountId, intent);
}

View File

@ -29,7 +29,6 @@ import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
import com.android.email.Email;
import com.android.email.Preferences;
import com.android.email.SecurityPolicy;
import com.android.email.VendorPolicyLoader;
@ -106,15 +105,7 @@ public class EmailBroadcastProcessorService extends IntentService {
if (Intent.ACTION_BOOT_COMPLETED.equals(broadcastAction)) {
onBootCompleted();
// TODO: Do a better job when we get ACTION_DEVICE_STORAGE_LOW.
// The code below came from very old code....
} else if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(broadcastAction)) {
// Stop IMAP/POP3 poll.
MailService.actionCancel(this);
} else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(broadcastAction)) {
enableComponentsIfNecessary();
} else if (ACTION_SECRET_CODE.equals(broadcastAction)
} else if (ACTION_SECRET_CODE.equals(broadcastAction)
&& SECRET_CODE_HOST_DEBUG_SCREEN.equals(broadcastIntent.getData().getHost())) {
AccountSettings.actionSettingsWithDebug(this);
} else if (AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION.equals(broadcastAction)) {
@ -126,22 +117,12 @@ public class EmailBroadcastProcessorService extends IntentService {
}
}
private void enableComponentsIfNecessary() {
if (Email.setServicesEnabledSync(this)) {
// At least one account exists.
// TODO probably we should check if it's a POP/IMAP account.
MailService.actionReschedule(this);
}
}
/**
* Handles {@link Intent#ACTION_BOOT_COMPLETED}. Called on a worker thread.
*/
private void onBootCompleted() {
performOneTimeInitialization();
enableComponentsIfNecessary();
// Starts the service for Exchange, if supported.
EmailServiceUtils.startExchangeService(this);
}

View File

@ -0,0 +1,517 @@
/* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.email.service;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.TrafficStats;
import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
import com.android.email.Email;
import com.android.email.LegacyConversions;
import com.android.email.NotificationController;
import com.android.email.Controller.Result;
import com.android.email.mail.Sender;
import com.android.email.mail.Store;
import com.android.email.provider.Utilities;
import com.android.emailcommon.AccountManagerTypes;
import com.android.emailcommon.Api;
import com.android.emailcommon.Logging;
import com.android.emailcommon.TrafficFlags;
import com.android.emailcommon.internet.MimeBodyPart;
import com.android.emailcommon.internet.MimeHeader;
import com.android.emailcommon.internet.MimeMultipart;
import com.android.emailcommon.mail.AuthenticationFailedException;
import com.android.emailcommon.mail.FetchProfile;
import com.android.emailcommon.mail.Folder;
import com.android.emailcommon.mail.Message;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.mail.Folder.MessageRetrievalListener;
import com.android.emailcommon.mail.Folder.OpenMode;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.provider.EmailContent.Attachment;
import com.android.emailcommon.provider.EmailContent.MailboxColumns;
import com.android.emailcommon.provider.EmailContent.MessageColumns;
import com.android.emailcommon.service.EmailServiceStatus;
import com.android.emailcommon.service.IEmailService;
import com.android.emailcommon.service.IEmailServiceCallback;
import com.android.emailcommon.service.SearchParams;
import com.android.emailcommon.utility.AttachmentUtilities;
import com.android.emailcommon.utility.Utility;
import java.io.IOException;
import java.util.HashSet;
/**
* EmailServiceStub is an abstract class representing an EmailService
*
* This class provides legacy support for a few methods that are common to both
* IMAP and POP3, including startSync, loadMore, loadAttachment, and sendMail
*/
public abstract class EmailServiceStub extends IEmailService.Stub implements IEmailService {
private static final int MAILBOX_COLUMN_ID = 0;
private static final int MAILBOX_COLUMN_SERVER_ID = 1;
private static final int MAILBOX_COLUMN_TYPE = 2;
/** Small projection for just the columns required for a sync. */
private static final String[] MAILBOX_PROJECTION = new String[] {
MailboxColumns.ID,
MailboxColumns.SERVER_ID,
MailboxColumns.TYPE,
};
private Context mContext;
private IEmailServiceCallback.Stub mCallback;
protected void init(Context context, IEmailServiceCallback.Stub callbackProxy) {
mContext = context;
mCallback = callbackProxy;
}
@Override
public Bundle validate(HostAuth hostauth) throws RemoteException {
// TODO Auto-generated method stub
return null;
}
@Override
public void startSync(long mailboxId, boolean userRequest) throws RemoteException {
Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId);
if (mailbox == null) return;
Account account = Account.restoreAccountWithId(mContext, mailbox.mAccountKey);
if (account == null) return;
android.accounts.Account acct = new android.accounts.Account(account.mEmailAddress,
AccountManagerTypes.TYPE_POP_IMAP);
Bundle extras = new Bundle();
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
ContentResolver.requestSync(acct, EmailContent.AUTHORITY, extras);
}
@Override
public void stopSync(long mailboxId) throws RemoteException {
// Not required
}
@Override
public void loadMore(long messageId) throws RemoteException {
// Load a message for view...
try {
// 1. Resample the message, in case it disappeared or synced while
// this command was in queue
EmailContent.Message message =
EmailContent.Message.restoreMessageWithId(mContext, messageId);
if (message == null) {
mCallback.loadMessageStatus(messageId,
EmailServiceStatus.MESSAGE_NOT_FOUND, 0);
return;
}
if (message.mFlagLoaded == EmailContent.Message.FLAG_LOADED_COMPLETE) {
// We should NEVER get here
mCallback.loadMessageStatus(messageId, 0, 100);
return;
}
// 2. Open the remote folder.
// TODO combine with common code in loadAttachment
Account account = Account.restoreAccountWithId(mContext, message.mAccountKey);
Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey);
if (account == null || mailbox == null) {
//mListeners.loadMessageForViewFailed(messageId, "null account or mailbox");
return;
}
TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account));
Store remoteStore = Store.getInstance(account, mContext);
String remoteServerId = mailbox.mServerId;
// If this is a search result, use the protocolSearchInfo field to get the
// correct remote location
if (!TextUtils.isEmpty(message.mProtocolSearchInfo)) {
remoteServerId = message.mProtocolSearchInfo;
}
Folder remoteFolder = remoteStore.getFolder(remoteServerId);
remoteFolder.open(OpenMode.READ_WRITE);
// 3. Set up to download the entire message
Message remoteMessage = remoteFolder.getMessage(message.mServerId);
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.BODY);
remoteFolder.fetch(new Message[] { remoteMessage }, fp, null);
// 4. Write to provider
Utilities.copyOneMessageToProvider(mContext, remoteMessage, account, mailbox,
EmailContent.Message.FLAG_LOADED_COMPLETE);
// 5. Notify UI
mCallback.loadMessageStatus(messageId, 0, 100);
} catch (MessagingException me) {
if (Logging.LOGD) Log.v(Logging.LOG_TAG, "", me);
mCallback.loadMessageStatus(messageId, EmailServiceStatus.REMOTE_EXCEPTION, 0);
} catch (RuntimeException rte) {
mCallback.loadMessageStatus(messageId, EmailServiceStatus.REMOTE_EXCEPTION, 0);
}
}
@Override
public void loadAttachment(long attachmentId, boolean background) throws RemoteException {
try {
//1. Check if the attachment is already here and return early in that case
Attachment attachment =
Attachment.restoreAttachmentWithId(mContext, attachmentId);
if (attachment == null) {
// mListeners.loadAttachmentFailed(accountId, messageId, attachmentId,
// new MessagingException("The attachment is null"),
// background);
return;
}
if (Utility.attachmentExists(mContext, attachment)) {
// mListeners.loadAttachmentFinished(accountId, messageId, attachmentId);
return;
}
EmailContent.Message message =
EmailContent.Message.restoreMessageWithId(mContext, attachment.mMessageKey);
// 2. Open the remote folder.
// TODO all of these could be narrower projections
Account account = Account.restoreAccountWithId(mContext, message.mAccountKey);
Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey);
if (account == null || mailbox == null || message == null) {
// mListeners.loadAttachmentFailed(accountId, messageId, attachmentId,
// new MessagingException(
// "Account, mailbox, message or attachment are null"),
// background);
return;
}
TrafficStats.setThreadStatsTag(
TrafficFlags.getAttachmentFlags(mContext, account));
Store remoteStore = Store.getInstance(account, mContext);
Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
remoteFolder.open(OpenMode.READ_WRITE);
// 3. Generate a shell message in which to retrieve the attachment,
// and a shell BodyPart for the attachment. Then glue them together.
Message storeMessage = remoteFolder.createMessage(message.mServerId);
MimeBodyPart storePart = new MimeBodyPart();
storePart.setSize((int)attachment.mSize);
storePart.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA,
attachment.mLocation);
storePart.setHeader(MimeHeader.HEADER_CONTENT_TYPE,
String.format("%s;\n name=\"%s\"",
attachment.mMimeType,
attachment.mFileName));
// TODO is this always true for attachments? I think we dropped the
// true encoding along the way
storePart.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
MimeMultipart multipart = new MimeMultipart();
multipart.setSubType("mixed");
multipart.addBodyPart(storePart);
storeMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed");
storeMessage.setBody(multipart);
// 4. Now ask for the attachment to be fetched
FetchProfile fp = new FetchProfile();
fp.add(storePart);
remoteFolder.fetch(new Message[] { storeMessage }, fp,
new MessageRetrievalListenerBridge(message.mId, attachmentId));
// If we failed to load the attachment, throw an Exception here, so that
// AttachmentDownloadService knows that we failed
if (storePart.getBody() == null) {
throw new MessagingException("Attachment not loaded.");
}
// 5. Save the downloaded file and update the attachment as necessary
LegacyConversions.saveAttachmentBody(mContext, storePart, attachment,
message.mAccountKey);
// 6. Report success
// mListeners.loadAttachmentFinished(accountId, messageId, attachmentId);
}
catch (MessagingException me) {
if (Logging.LOGD) Log.v(Logging.LOG_TAG, "", me);
// mListeners.loadAttachmentFailed(
// accountId, messageId, attachmentId, me, background);
} catch (IOException ioe) {
Log.e(Logging.LOG_TAG, "Error while storing attachment." + ioe.toString());
}
}
// TODO: Implement callback
@Override
public void updateFolderList(long accountId) throws RemoteException {
Account account = Account.restoreAccountWithId(mContext, accountId);
if (account == null) return;
TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account));
Cursor localFolderCursor = null;
try {
// Step 1: Get remote mailboxes
Store store = Store.getInstance(account, mContext);
Folder[] remoteFolders = store.updateFolders();
HashSet<String> remoteFolderNames = new HashSet<String>();
for (int i = 0, count = remoteFolders.length; i < count; i++) {
remoteFolderNames.add(remoteFolders[i].getName());
}
// Step 2: Get local mailboxes
localFolderCursor = mContext.getContentResolver().query(
Mailbox.CONTENT_URI,
MAILBOX_PROJECTION,
EmailContent.MailboxColumns.ACCOUNT_KEY + "=?",
new String[] { String.valueOf(account.mId) },
null);
// Step 3: Remove any local mailbox not on the remote list
while (localFolderCursor.moveToNext()) {
String mailboxPath = localFolderCursor.getString(MAILBOX_COLUMN_SERVER_ID);
// Short circuit if we have a remote mailbox with the same name
if (remoteFolderNames.contains(mailboxPath)) {
continue;
}
int mailboxType = localFolderCursor.getInt(MAILBOX_COLUMN_TYPE);
long mailboxId = localFolderCursor.getLong(MAILBOX_COLUMN_ID);
switch (mailboxType) {
case Mailbox.TYPE_INBOX:
case Mailbox.TYPE_DRAFTS:
case Mailbox.TYPE_OUTBOX:
case Mailbox.TYPE_SENT:
case Mailbox.TYPE_TRASH:
case Mailbox.TYPE_SEARCH:
// Never, ever delete special mailboxes
break;
default:
// Drop all attachment files related to this mailbox
AttachmentUtilities.deleteAllMailboxAttachmentFiles(
mContext, accountId, mailboxId);
// Delete the mailbox; database triggers take care of related
// Message, Body and Attachment records
Uri uri = ContentUris.withAppendedId(
Mailbox.CONTENT_URI, mailboxId);
mContext.getContentResolver().delete(uri, null, null);
break;
}
}
//mListeners.listFoldersFinished(accountId);
} catch (Exception e) {
//mListeners.listFoldersFailed(accountId, e.toString());
} finally {
if (localFolderCursor != null) {
localFolderCursor.close();
}
}
}
@Override
public boolean createFolder(long accountId, String name) throws RemoteException {
// Not required
return false;
}
@Override
public boolean deleteFolder(long accountId, String name) throws RemoteException {
// Not required
return false;
}
@Override
public boolean renameFolder(long accountId, String oldName, String newName)
throws RemoteException {
// Not required
return false;
}
@Override
public void setCallback(IEmailServiceCallback cb) throws RemoteException {
// Not required
}
@Override
public void setLogging(int on) throws RemoteException {
// Not required
}
@Override
public void hostChanged(long accountId) throws RemoteException {
// Not required
}
@Override
public Bundle autoDiscover(String userName, String password) throws RemoteException {
// Not required
return null;
}
@Override
public void sendMeetingResponse(long messageId, int response) throws RemoteException {
// Not required
}
@Override
public void deleteAccountPIMData(long accountId) throws RemoteException {
// Not required
}
@Override
public int getApiLevel() throws RemoteException {
return Api.LEVEL;
}
@Override
public int searchMessages(long accountId, SearchParams params, long destMailboxId)
throws RemoteException {
// Not required
return 0;
}
@Override
public void sendMail(long accountId) throws RemoteException {
Account account = Account.restoreAccountWithId(mContext, accountId);
TrafficStats.setThreadStatsTag(TrafficFlags.getSmtpFlags(mContext, account));
NotificationController nc = NotificationController.getInstance(mContext);
// 1. Loop through all messages in the account's outbox
long outboxId = Mailbox.findMailboxOfType(mContext, account.mId, Mailbox.TYPE_OUTBOX);
if (outboxId == Mailbox.NO_MAILBOX) {
return;
}
ContentResolver resolver = mContext.getContentResolver();
Cursor c = resolver.query(EmailContent.Message.CONTENT_URI,
EmailContent.Message.ID_COLUMN_PROJECTION,
EmailContent.Message.MAILBOX_KEY + "=?", new String[] { Long.toString(outboxId) },
null);
try {
// 2. exit early
if (c.getCount() <= 0) {
return;
}
// 3. do one-time setup of the Sender & other stuff
//mListeners.sendPendingMessagesStarted(account.mId, -1);
Sender sender = Sender.getInstance(mContext, account);
Store remoteStore = Store.getInstance(account, mContext);
boolean requireMoveMessageToSentFolder = remoteStore.requireCopyMessageToSentFolder();
ContentValues moveToSentValues = null;
if (requireMoveMessageToSentFolder) {
Mailbox sentFolder =
Mailbox.restoreMailboxOfType(mContext, accountId, Mailbox.TYPE_SENT);
moveToSentValues = new ContentValues();
moveToSentValues.put(MessageColumns.MAILBOX_KEY, sentFolder.mId);
}
// 4. loop through the available messages and send them
while (c.moveToNext()) {
long messageId = -1;
try {
messageId = c.getLong(0);
//mListeners.sendPendingMessagesStarted(account.mId, messageId);
// Don't send messages with unloaded attachments
if (Utility.hasUnloadedAttachments(mContext, messageId)) {
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, "Can't send #" + messageId +
"; unloaded attachments");
}
continue;
}
sender.sendMessage(messageId);
} catch (MessagingException me) {
// report error for this message, but keep trying others
if (me instanceof AuthenticationFailedException) {
nc.showLoginFailedNotification(account.mId);
}
//mListeners.sendPendingMessagesFailed(account.mId, messageId, me);
continue;
}
// 5. move to sent, or delete
Uri syncedUri =
ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId);
if (requireMoveMessageToSentFolder) {
// If this is a forwarded message and it has attachments, delete them, as they
// duplicate information found elsewhere (on the server). This saves storage.
EmailContent.Message msg =
EmailContent.Message.restoreMessageWithId(mContext, messageId);
if (msg != null &&
((msg.mFlags & EmailContent.Message.FLAG_TYPE_FORWARD) != 0)) {
AttachmentUtilities.deleteAllAttachmentFiles(mContext, account.mId,
messageId);
}
resolver.update(syncedUri, moveToSentValues, null, null);
} else {
AttachmentUtilities.deleteAllAttachmentFiles(mContext, account.mId,
messageId);
Uri uri =
ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId);
resolver.delete(uri, null, null);
resolver.delete(syncedUri, null, null);
}
}
// 6. report completion/success
//mListeners.sendPendingMessagesCompleted(account.mId);
nc.cancelLoginFailedNotification(account.mId);
} catch (MessagingException me) {
if (me instanceof AuthenticationFailedException) {
nc.showLoginFailedNotification(account.mId);
}
//mListeners.sendPendingMessagesFailed(account.mId, -1, me);
} finally {
c.close();
}
}
/**
* Bridge to intercept {@link MessageRetrievalListener#loadAttachmentProgress} and
* pass down to {@link Result}.
*/
public class MessageRetrievalListenerBridge implements MessageRetrievalListener {
private final long mMessageId;
// private final long mAttachmentId;
// private final long mAccountId;
public MessageRetrievalListenerBridge(long messageId, long attachmentId) {
mMessageId = messageId;
// mAttachmentId = attachmentId;
// mAccountId = Account.getAccountIdForMessageId(mContext, mMessageId);
}
@Override
public void loadAttachmentProgress(int progress) {
// synchronized (mListeners) {
// for (Result listener : mListeners) {
// listener.loadAttachmentCallback(null, mAccountId, mMessageId, mAttachmentId,
// progress);
// }
// }
}
@Override
public void messageRetrieved(com.android.emailcommon.mail.Message message) {
}
}
}

View File

@ -16,19 +16,13 @@
package com.android.email.service;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import com.android.emailcommon.Api;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.service.EmailServiceProxy;
import com.android.emailcommon.service.IEmailService;
import com.android.emailcommon.service.IEmailServiceCallback;
import com.android.emailcommon.service.SearchParams;
/**
* Utility functions for EmailService support.
@ -74,106 +68,32 @@ public class EmailServiceUtils {
return new EmailServiceProxy(context, ImapService.class, callback);
}
public static EmailServiceProxy getPop3Service(Context context,
IEmailServiceCallback callback) {
return new EmailServiceProxy(context, Pop3Service.class, callback);
}
public static boolean isExchangeAvailable(Context context) {
return isServiceAvailable(context, EmailServiceProxy.EXCHANGE_INTENT);
}
/**
* An empty {@link IEmailService} implementation which is used instead of
* {@link com.android.exchange.ExchangeService} on the build with no exchange support.
* For a given account id, return a service proxy if applicable, or null.
*
* <p>In theory, the service in question isn't used on the no-exchange-support build,
* because we won't have any exchange accounts in that case, so we wouldn't have to have this
* class. However, there are a few places we do use the service even if there's no exchange
* accounts (e.g. setLogging), so this class is added for safety and simplicity.
* @param accountId the message of interest
* @result service proxy, or null if n/a
*/
public static class NullEmailService extends Service implements IEmailService {
public static final NullEmailService INSTANCE = new NullEmailService();
@Override
public int getApiLevel() {
return Api.LEVEL;
}
@Override
public Bundle autoDiscover(String userName, String password) throws RemoteException {
return Bundle.EMPTY;
}
@Override
public boolean createFolder(long accountId, String name) throws RemoteException {
return false;
}
@Override
public boolean deleteFolder(long accountId, String name) throws RemoteException {
return false;
}
@Override
public void hostChanged(long accountId) throws RemoteException {
}
@Override
public void loadAttachment(long attachmentId, boolean background) throws RemoteException {
}
@Override
public void loadMore(long messageId) throws RemoteException {
}
@Override
public boolean renameFolder(long accountId, String oldName, String newName)
throws RemoteException {
return false;
}
@Override
public void sendMeetingResponse(long messageId, int response) throws RemoteException {
}
@Override
public void setCallback(IEmailServiceCallback cb) throws RemoteException {
}
@Override
public void setLogging(int flags) throws RemoteException {
}
@Override
public void startSync(long mailboxId, boolean userRequest) throws RemoteException {
}
@Override
public void stopSync(long mailboxId) throws RemoteException {
}
@Override
public void updateFolderList(long accountId) throws RemoteException {
}
@Override
public Bundle validate(HostAuth hostAuth) throws RemoteException {
return null;
}
@Override
public void deleteAccountPIMData(long accountId) throws RemoteException {
}
@Override
public int searchMessages(long accountId, SearchParams searchParams, long destMailboxId) {
return 0;
}
@Override
public IBinder asBinder() {
return null;
}
@Override
public IBinder onBind(Intent intent) {
return null;
public static EmailServiceProxy getServiceForAccount(Context context,
IEmailServiceCallback callback, long accountId) {
String protocol = Account.getProtocol(context, accountId);
if (protocol.equals(HostAuth.SCHEME_IMAP)) {
return getImapService(context, callback);
} else if (protocol.equals(HostAuth.SCHEME_POP3)) {
return getPop3Service(context, callback);
} else if (protocol.equals(HostAuth.SCHEME_EAS)) {
return getExchangeService(context, callback);
} else {
throw new IllegalArgumentException("Account with unknown protocol: " + accountId);
}
}
}

View File

@ -25,7 +25,6 @@ import android.content.Intent;
import android.database.Cursor;
import android.net.TrafficStats;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
@ -36,8 +35,7 @@ import com.android.email.Email;
import com.android.email.LegacyConversions;
import com.android.email.NotificationController;
import com.android.email.mail.Store;
import com.android.emailcommon.AccountManagerTypes;
import com.android.emailcommon.Api;
import com.android.email.provider.Utilities;
import com.android.emailcommon.Logging;
import com.android.emailcommon.TrafficFlags;
import com.android.emailcommon.internet.MimeUtility;
@ -57,16 +55,10 @@ import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.MailboxColumns;
import com.android.emailcommon.provider.EmailContent.MessageColumns;
import com.android.emailcommon.provider.EmailContent.SyncColumns;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.service.EmailServiceStatus;
import com.android.emailcommon.service.IEmailService;
import com.android.emailcommon.service.IEmailServiceCallback;
import com.android.emailcommon.service.SearchParams;
import com.android.emailcommon.utility.AttachmentUtilities;
import com.android.emailcommon.utility.ConversionUtilities;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
@ -81,6 +73,14 @@ public class ImapService extends Service {
private static final Flag[] FLAG_LIST_FLAGGED = new Flag[] { Flag.FLAGGED };
private static final Flag[] FLAG_LIST_ANSWERED = new Flag[] { Flag.ANSWERED };
/**
* Simple cache for last search result mailbox by account and serverId, since the most common
* case will be repeated use of the same mailbox
*/
private static long mLastSearchAccountKey = Account.NO_ACCOUNT;
private static String mLastSearchServerId = null;
private static Mailbox mLastSearchRemoteMailbox = null;
/**
* Cache search results by account; this allows for "load more" support without having to
* redo the search (which can be quite slow). SortableMessage is a smallish class, so memory
@ -203,164 +203,27 @@ public class ImapService extends Service {
/**
* Create our EmailService implementation here.
*/
private final IEmailService.Stub mBinder = new IEmailService.Stub() {
@Override
public int getApiLevel() {
return Api.LEVEL;
}
@Override
public Bundle validate(HostAuth hostAuth) throws RemoteException {
return null;
}
@Override
public Bundle autoDiscover(String userName, String password) throws RemoteException {
return null;
}
@Override
public void startSync(long mailboxId, boolean userRequest) throws RemoteException {
Context context = getApplicationContext();
Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
if (mailbox == null) return;
Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey);
if (account == null) return;
android.accounts.Account acct = new android.accounts.Account(account.mEmailAddress,
AccountManagerTypes.TYPE_POP_IMAP);
Log.d(TAG, "startSync API requesting sync");
Bundle extras = new Bundle();
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
ContentResolver.requestSync(acct, EmailContent.AUTHORITY, extras);
}
@Override
public void stopSync(long mailboxId) throws RemoteException {
}
@Override
public void loadAttachment(long attachmentId, boolean background) throws RemoteException {
}
@Override
public void updateFolderList(long accountId) throws RemoteException {
}
@Override
public void hostChanged(long accountId) throws RemoteException {
}
@Override
public void setLogging(int flags) throws RemoteException {
}
@Override
public void sendMeetingResponse(long messageId, int response) throws RemoteException {
}
@Override
public void loadMore(long messageId) throws RemoteException {
// Load a message for view...
Context context = getApplicationContext();
try {
// 1. Resample the message, in case it disappeared or synced while
// this command was in queue
EmailContent.Message message =
EmailContent.Message.restoreMessageWithId(context, messageId);
if (message == null) {
sCallbackProxy.loadMessageStatus(messageId,
EmailServiceStatus.MESSAGE_NOT_FOUND, 0);
return;
}
if (message.mFlagLoaded == EmailContent.Message.FLAG_LOADED_COMPLETE) {
// We should NEVER get here
sCallbackProxy.loadMessageStatus(messageId, 0, 100);
return;
}
// 2. Open the remote folder.
// TODO combine with common code in loadAttachment
Account account = Account.restoreAccountWithId(context, message.mAccountKey);
Mailbox mailbox = Mailbox.restoreMailboxWithId(context, message.mMailboxKey);
if (account == null || mailbox == null) {
//mListeners.loadMessageForViewFailed(messageId, "null account or mailbox");
return;
}
TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account));
Store remoteStore = Store.getInstance(account, context);
String remoteServerId = mailbox.mServerId;
// If this is a search result, use the protocolSearchInfo field to get the
// correct remote location
if (!TextUtils.isEmpty(message.mProtocolSearchInfo)) {
remoteServerId = message.mProtocolSearchInfo;
}
Folder remoteFolder = remoteStore.getFolder(remoteServerId);
remoteFolder.open(OpenMode.READ_WRITE);
// 3. Set up to download the entire message
Message remoteMessage = remoteFolder.getMessage(message.mServerId);
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.BODY);
remoteFolder.fetch(new Message[] { remoteMessage }, fp, null);
// 4. Write to provider
copyOneMessageToProvider(context, remoteMessage, account, mailbox,
EmailContent.Message.FLAG_LOADED_COMPLETE);
// 5. Notify UI
sCallbackProxy.loadMessageStatus(messageId, 0, 100);
} catch (MessagingException me) {
if (Logging.LOGD) Log.v(Logging.LOG_TAG, "", me);
sCallbackProxy.loadMessageStatus(messageId, EmailServiceStatus.REMOTE_EXCEPTION, 0);
} catch (RuntimeException rte) {
sCallbackProxy.loadMessageStatus(messageId, EmailServiceStatus.REMOTE_EXCEPTION, 0);
}
}
// The following three methods are not implemented in this version
@Override
public boolean createFolder(long accountId, String name) throws RemoteException {
return false;
}
@Override
public boolean deleteFolder(long accountId, String name) throws RemoteException {
return false;
}
@Override
public boolean renameFolder(long accountId, String oldName, String newName)
throws RemoteException {
return false;
}
private final EmailServiceStub mBinder = new EmailServiceStub() {
@Override
public void setCallback(IEmailServiceCallback cb) throws RemoteException {
mCallbackList.register(cb);
}
/**
* Delete PIM (calendar, contacts) data for the specified account
*
* @param accountId the account whose data should be deleted
* @throws RemoteException
*/
@Override
public void deleteAccountPIMData(long accountId) throws RemoteException {
}
@Override
public int searchMessages(long accountId, SearchParams searchParams, long destMailboxId) {
try {
return searchMailboxImpl(getApplicationContext(), accountId, searchParams,
destMailboxId);
} catch (MessagingException e) {
}
return 0;
}
};
@Override
public IBinder onBind(Intent intent) {
mBinder.init(this, sCallbackProxy);
return mBinder;
}
@ -380,11 +243,7 @@ public class ImapService extends Service {
NotificationController nc = NotificationController.getInstance(context);
try {
processPendingActionsSynchronous(context, account);
// Select generic sync or store-specific sync
SyncResults results = synchronizeMailboxGeneric(context, account, folder);
// The account might have been deleted
if (results == null) return;
synchronizeMailboxGeneric(context, account, folder);
// Clear authentication notification for this account
nc.cancelLoginFailedNotification(account.mId);
} catch (MessagingException e) {
@ -436,14 +295,6 @@ public class ImapService extends Service {
}
}
private static void saveOrUpdate(EmailContent content, Context context) {
if (content.isSaved()) {
content.update(context, content.toContentValues());
} else {
content.save(context);
}
}
/**
* Load the structure and body of messages not yet synced
* @param account the account we're syncing
@ -488,7 +339,7 @@ public class ImapService extends Service {
@Override
public void messageRetrieved(Message message) {
// Store the updated message locally and mark it fully loaded
copyOneMessageToProvider(context, message, account, toMailbox,
Utilities.copyOneMessageToProvider(context, message, account, toMailbox,
EmailContent.Message.FLAG_LOADED_COMPLETE);
}
@ -515,7 +366,7 @@ public class ImapService extends Service {
remoteFolder.fetch(new Message[] { message }, fp, null);
// Store the partially-loaded message and mark it partially loaded
copyOneMessageToProvider(context, message, account, toMailbox,
Utilities.copyOneMessageToProvider(context, message, account, toMailbox,
EmailContent.Message.FLAG_LOADED_PARTIAL);
} else {
// We have a structure to deal with, from which
@ -534,7 +385,7 @@ public class ImapService extends Service {
remoteFolder.fetch(new Message[] { message }, fp, null);
}
// Store the updated message locally and mark it fully loaded
copyOneMessageToProvider(context, message, account, toMailbox,
Utilities.copyOneMessageToProvider(context, message, account, toMailbox,
EmailContent.Message.FLAG_LOADED_COMPLETE);
}
}
@ -579,7 +430,7 @@ public class ImapService extends Service {
LegacyConversions.updateMessageFields(localMessage,
message, account.mId, mailbox.mId);
// Commit the message to the local store
saveOrUpdate(localMessage, context);
Utilities.saveOrUpdate(localMessage, context);
// Track the "new" ness of the downloaded message
if (!message.isSet(Flag.SEEN) && unseenMessages != null) {
unseenMessages.add(localMessage.mId);
@ -614,7 +465,7 @@ public class ImapService extends Service {
* @return results of the sync pass
* @throws MessagingException
*/
private static SyncResults synchronizeMailboxGeneric(final Context context,
private static void synchronizeMailboxGeneric(final Context context,
final Account account, final Mailbox mailbox) throws MessagingException {
/*
@ -630,8 +481,7 @@ public class ImapService extends Service {
// 0. We do not ever sync DRAFTS or OUTBOX (down or up)
if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) {
int totalMessages = EmailContent.count(context, mailbox.getUri(), null, null);
return new SyncResults(totalMessages, unseenMessages);
return;
}
// 1. Get the message list from the local store and create an index of the uids
@ -664,7 +514,7 @@ public class ImapService extends Service {
Store remoteStore = Store.getInstance(account, context);
// The account might have been deleted
if (remoteStore == null) return null;
if (remoteStore == null) return;
Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
/*
@ -678,7 +528,7 @@ public class ImapService extends Service {
|| mailbox.mType == Mailbox.TYPE_DRAFTS) {
if (!remoteFolder.exists()) {
if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) {
return new SyncResults(0, unseenMessages);
return;
}
}
}
@ -829,107 +679,6 @@ public class ImapService extends Service {
// 14. Clean up and report results
remoteFolder.close(false);
return new SyncResults(remoteMessageCount, unseenMessages);
}
/**
* Copy one downloaded message (which may have partially-loaded sections)
* into a newly created EmailProvider Message, given the account and mailbox
*
* @param message the remote message we've just downloaded
* @param account the account it will be stored into
* @param folder the mailbox it will be stored into
* @param loadStatus when complete, the message will be marked with this status (e.g.
* EmailContent.Message.LOADED)
*/
public static void copyOneMessageToProvider(Context context, Message message, Account account,
Mailbox folder, int loadStatus) {
EmailContent.Message localMessage = null;
Cursor c = null;
try {
c = context.getContentResolver().query(
EmailContent.Message.CONTENT_URI,
EmailContent.Message.CONTENT_PROJECTION,
EmailContent.MessageColumns.ACCOUNT_KEY + "=?" +
" AND " + MessageColumns.MAILBOX_KEY + "=?" +
" AND " + SyncColumns.SERVER_ID + "=?",
new String[] {
String.valueOf(account.mId),
String.valueOf(folder.mId),
String.valueOf(message.getUid())
},
null);
if (c.moveToNext()) {
localMessage = EmailContent.getContent(c, EmailContent.Message.class);
localMessage.mMailboxKey = folder.mId;
localMessage.mAccountKey = account.mId;
copyOneMessageToProvider(context, message, localMessage, loadStatus);
}
} finally {
if (c != null) {
c.close();
}
}
}
/**
* Copy one downloaded message (which may have partially-loaded sections)
* into an already-created EmailProvider Message
*
* @param message the remote message we've just downloaded
* @param localMessage the EmailProvider Message, already created
* @param loadStatus when complete, the message will be marked with this status (e.g.
* EmailContent.Message.LOADED)
* @param context the context to be used for EmailProvider
*/
public static void copyOneMessageToProvider(Context context, Message message,
EmailContent.Message localMessage, int loadStatus) {
try {
EmailContent.Body body = EmailContent.Body.restoreBodyWithMessageId(context,
localMessage.mId);
if (body == null) {
body = new EmailContent.Body();
}
try {
// Copy the fields that are available into the message object
LegacyConversions.updateMessageFields(localMessage, message,
localMessage.mAccountKey, localMessage.mMailboxKey);
// Now process body parts & attachments
ArrayList<Part> viewables = new ArrayList<Part>();
ArrayList<Part> attachments = new ArrayList<Part>();
MimeUtility.collectParts(message, viewables, attachments);
ConversionUtilities.updateBodyFields(body, localMessage, viewables);
// Commit the message & body to the local store immediately
saveOrUpdate(localMessage, context);
saveOrUpdate(body, context);
// process (and save) attachments
LegacyConversions.updateAttachments(context, localMessage, attachments);
// One last update of message with two updated flags
localMessage.mFlagLoaded = loadStatus;
ContentValues cv = new ContentValues();
cv.put(EmailContent.MessageColumns.FLAG_ATTACHMENT, localMessage.mFlagAttachment);
cv.put(EmailContent.MessageColumns.FLAG_LOADED, localMessage.mFlagLoaded);
Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI,
localMessage.mId);
context.getContentResolver().update(uri, cv, null, null);
} catch (MessagingException me) {
Log.e(Logging.LOG_TAG, "Error while copying downloaded message." + me);
}
} catch (RuntimeException rte) {
Log.e(Logging.LOG_TAG, "Error while storing downloaded message." + rte.toString());
} catch (IOException ioe) {
Log.e(Logging.LOG_TAG, "Error while storing attachment." + ioe.toString());
}
}
/**
@ -975,10 +724,10 @@ public class ImapService extends Service {
if (!TextUtils.isEmpty(message.mProtocolSearchInfo)) {
long accountKey = message.mAccountKey;
String protocolSearchInfo = message.mProtocolSearchInfo;
// if (accountKey == mLastSearchAccountKey &&
// protocolSearchInfo.equals(mLastSearchServerId)) {
// return mLastSearchRemoteMailbox;
// }
if (accountKey == mLastSearchAccountKey &&
protocolSearchInfo.equals(mLastSearchServerId)) {
return mLastSearchRemoteMailbox;
}
Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI,
Mailbox.CONTENT_PROJECTION, Mailbox.PATH_AND_ACCOUNT_SELECTION,
new String[] {protocolSearchInfo, Long.toString(accountKey)},
@ -987,9 +736,9 @@ public class ImapService extends Service {
if (c.moveToNext()) {
Mailbox mailbox = new Mailbox();
mailbox.restore(c);
// mLastSearchAccountKey = accountKey;
// mLastSearchServerId = protocolSearchInfo;
// mLastSearchRemoteMailbox = mailbox;
mLastSearchAccountKey = accountKey;
mLastSearchServerId = protocolSearchInfo;
mLastSearchRemoteMailbox = mailbox;
return mailbox;
} else {
return null;
@ -1574,22 +1323,6 @@ public class ImapService extends Service {
remoteTrashFolder.close(false);
}
/** Results of the latest synchronization. */
private static class SyncResults {
/** The total # of messages in the folder */
public final int mTotalMessages;
/** A list of new message IDs; must not be {@code null} */
public final ArrayList<Long> mAddedMessages;
public SyncResults(int totalMessages, ArrayList<Long> addedMessages) {
if (addedMessages == null) {
throw new IllegalArgumentException("addedMessages must not be null");
}
mTotalMessages = totalMessages;
mAddedMessages = addedMessages;
}
}
/**
* A message and numeric uid that's easily sortable
*/
@ -1686,7 +1419,7 @@ public class ImapService extends Service {
LegacyConversions.updateMessageFields(localMessage,
message, account.mId, mailbox.mId);
// Commit the message to the local store
saveOrUpdate(localMessage, context);
Utilities.saveOrUpdate(localMessage, context);
localMessage.mMailboxKey = destMailboxId;
// We load 50k or so; maybe it's complete, maybe not...
int flag = EmailContent.Message.FLAG_LOADED_COMPLETE;
@ -1697,7 +1430,7 @@ public class ImapService extends Service {
if (message.getSize() > Store.FETCH_BODY_SANE_SUGGESTED_SIZE) {
flag = EmailContent.Message.FLAG_LOADED_PARTIAL;
}
copyOneMessageToProvider(context, message, localMessage, flag);
Utilities.copyOneMessageToProvider(context, message, localMessage, flag);
} catch (MessagingException me) {
Log.e(Logging.LOG_TAG,
"Error while copying downloaded message." + me);

View File

@ -18,99 +18,37 @@ package com.android.email.service;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.SyncStatusObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Log;
import com.android.email.Controller;
import com.android.email.Email;
import com.android.email.Preferences;
import com.android.email.SingleRunningTask;
import com.android.email.provider.AccountReconciler;
import com.android.emailcommon.AccountManagerTypes;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.utility.EmailAsyncTask;
import com.google.common.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* Background service for refreshing non-push email accounts.
*
* TODO: Convert to IntentService to move *all* work off the UI thread, serialize work, and avoid
* possible problems with out-of-order startId processing.
* Legacy service, now used mainly for account reconciliation
*/
public class MailService extends Service {
private static final String LOG_TAG = "Email-MailService";
private static final String ACTION_CHECK_MAIL =
"com.android.email.intent.action.MAIL_SERVICE_WAKEUP";
private static final String ACTION_RESCHEDULE =
"com.android.email.intent.action.MAIL_SERVICE_RESCHEDULE";
private static final String ACTION_CANCEL =
"com.android.email.intent.action.MAIL_SERVICE_CANCEL";
private static final String ACTION_SEND_PENDING_MAIL =
"com.android.email.intent.action.MAIL_SERVICE_SEND_PENDING";
private static final String EXTRA_ACCOUNT = "com.android.email.intent.extra.ACCOUNT";
private static final String EXTRA_ACCOUNT_INFO = "com.android.email.intent.extra.ACCOUNT_INFO";
private static final String EXTRA_DEBUG_WATCHDOG = "com.android.email.intent.extra.WATCHDOG";
/** Time between watchdog checks; in milliseconds */
private static final long WATCHDOG_DELAY = 10 * 60 * 1000; // 10 minutes
/** Sentinel value asking to update mSyncReports if it's currently empty */
@VisibleForTesting
static final int SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY = -1;
/** Sentinel value asking that mSyncReports be rebuilt */
@VisibleForTesting
static final int SYNC_REPORTS_RESET = -2;
@VisibleForTesting
Controller mController;
private final Controller.Result mControllerCallback = new ControllerResults();
private ContentResolver mContentResolver;
private Context mContext;
private int mStartId;
/**
* Access must be synchronized, because there are accesses from the Controller callback
*/
/*package*/ static HashMap<Long,AccountSyncReport> mSyncReports =
new HashMap<Long,AccountSyncReport>();
public static void actionReschedule(Context context) {
Intent i = new Intent();
i.setClass(context, MailService.class);
i.setAction(MailService.ACTION_RESCHEDULE);
context.startService(i);
}
public static void actionCancel(Context context) {
Intent i = new Intent();
i.setClass(context, MailService.class);
i.setAction(MailService.ACTION_CANCEL);
context.startService(i);
}
/**
* Entry point for AttachmentDownloadService to ask that pending mail be sent
@ -136,104 +74,21 @@ public class MailService extends Service {
}
});
// TODO this needs to be passed through the controller and back to us
mStartId = startId;
String action = intent.getAction();
final long accountId = intent.getLongExtra(EXTRA_ACCOUNT, -1);
mController = Controller.getInstance(this);
mController.addResultCallback(mControllerCallback);
mContentResolver = getContentResolver();
mContext = this;
final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
if ((ACTION_CHECK_MAIL).equals(action)) {
// DB access required to satisfy this intent, so offload from UI thread
EmailAsyncTask.runAsyncParallel(new Runnable() {
@Override
public void run() {
// If we have the data, restore the last-sync-times for each account
// These are cached in the wakeup intent in case the process was killed.
restoreSyncReports(intent);
// Sync a specific account if given
if (Email.DEBUG) {
Log.d(LOG_TAG, "action: check mail for id=" + accountId);
}
if (accountId >= 0) {
setWatchdog(accountId, alarmManager);
}
// Start sync if account is given && auto-sync is allowed
boolean syncStarted = false;
if (accountId != -1 && ContentResolver.getMasterSyncAutomatically()) {
synchronized(mSyncReports) {
for (AccountSyncReport report: mSyncReports.values()) {
if (report.accountId == accountId) {
// Only sync POP3 here (will remove POP3 sync soon)
if (report.syncEnabled &&
Account.getProtocol(MailService.this, accountId)
.equals(HostAuth.SCHEME_POP3)) {
syncStarted = syncOneAccount(mController, accountId,
startId);
}
break;
}
}
}
}
// Reschedule if we didn't start sync.
if (!syncStarted) {
// Prevent runaway on the current account by pretending it updated
if (accountId != -1) {
updateAccountReport(accountId, 0);
}
// Find next account to sync, and reschedule
reschedule(alarmManager);
// Stop the service, unless actually syncing (which will stop the service)
stopSelf(startId);
}
}
});
}
else if (ACTION_CANCEL.equals(action)) {
if (Email.DEBUG) {
Log.d(LOG_TAG, "action: cancel");
}
cancel();
stopSelf(startId);
}
else if (ACTION_SEND_PENDING_MAIL.equals(action)) {
if (ACTION_SEND_PENDING_MAIL.equals(action)) {
if (Email.DEBUG) {
Log.d(LOG_TAG, "action: send pending mail");
}
EmailAsyncTask.runAsyncParallel(new Runnable() {
@Override
public void run() {
mController.sendPendingMessages(accountId);
Controller.getInstance(getApplicationContext()).sendPendingMessages(accountId);
}
});
stopSelf(startId);
}
else if (ACTION_RESCHEDULE.equals(action)) {
if (Email.DEBUG) {
Log.d(LOG_TAG, "action: reschedule");
}
// DB access required to satisfy this intent, so offload from UI thread
EmailAsyncTask.runAsyncParallel(new Runnable() {
@Override
public void run() {
// When called externally, we refresh the sync reports table to pick up
// any changes in the account list or account settings
refreshSyncReports();
// Finally, scan for the next needing update, and set an alarm for it
reschedule(alarmManager);
stopSelf(startId);
}
});
}
// Returning START_NOT_STICKY means that if a mail check is killed (e.g. due to memory
// pressure, there will be no explicit restart. This is OK; Note that we set a watchdog
@ -247,390 +102,6 @@ public class MailService extends Service {
return null;
}
@Override
public void onDestroy() {
super.onDestroy();
Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback);
}
private void cancel() {
AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
PendingIntent pi = createAlarmIntent(-1, null, false);
alarmMgr.cancel(pi);
}
/**
* Refresh the sync reports, to pick up any changes in the account list or account settings.
*/
private void refreshSyncReports() {
synchronized (mSyncReports) {
// Make shallow copy of sync reports so we can recover the prev sync times
HashMap<Long,AccountSyncReport> oldSyncReports =
new HashMap<Long,AccountSyncReport>(mSyncReports);
// Delete the sync reports to force a refresh from live account db data
setupSyncReportsLocked(SYNC_REPORTS_RESET, this);
// Restore prev-sync & next-sync times for any reports in the new list
for (AccountSyncReport newReport : mSyncReports.values()) {
AccountSyncReport oldReport = oldSyncReports.get(newReport.accountId);
if (oldReport != null) {
newReport.prevSyncTime = oldReport.prevSyncTime;
newReport.setNextSyncTime();
}
}
}
}
/**
* Create and send an alarm with the entire list. This also sends a list of known last-sync
* times with the alarm, so if we are killed between alarms, we don't lose this info.
*
* @param alarmMgr passed in so we can mock for testing.
*/
private void reschedule(AlarmManager alarmMgr) {
// restore the reports if lost
setupSyncReports(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY);
synchronized (mSyncReports) {
int numAccounts = mSyncReports.size();
long[] accountInfo = new long[numAccounts * 2]; // pairs of { accountId, lastSync }
int accountInfoIndex = 0;
long nextCheckTime = Long.MAX_VALUE;
AccountSyncReport nextAccount = null;
long timeNow = SystemClock.elapsedRealtime();
for (AccountSyncReport report : mSyncReports.values()) {
if (report.syncInterval <= 0) { // no timed checks - skip
continue;
}
long prevSyncTime = report.prevSyncTime;
long nextSyncTime = report.nextSyncTime;
// select next account to sync
if ((prevSyncTime == 0) || (nextSyncTime < timeNow)) { // never checked, or overdue
nextCheckTime = 0;
nextAccount = report;
} else if (nextSyncTime < nextCheckTime) { // next to be checked
nextCheckTime = nextSyncTime;
nextAccount = report;
}
// collect last-sync-times for all accounts
// this is using pairs of {long,long} to simplify passing in a bundle
accountInfo[accountInfoIndex++] = report.accountId;
accountInfo[accountInfoIndex++] = report.prevSyncTime;
}
// Clear out any unused elements in the array
while (accountInfoIndex < accountInfo.length) {
accountInfo[accountInfoIndex++] = -1;
}
// set/clear alarm as needed
long idToCheck = (nextAccount == null) ? -1 : nextAccount.accountId;
PendingIntent pi = createAlarmIntent(idToCheck, accountInfo, false);
if (nextAccount == null) {
alarmMgr.cancel(pi);
if (Email.DEBUG) {
Log.d(LOG_TAG, "reschedule: alarm cancel - no account to check");
}
} else {
alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi);
if (Email.DEBUG) {
Log.d(LOG_TAG, "reschedule: alarm set at " + nextCheckTime
+ " for " + nextAccount);
}
}
}
}
/**
* Create a watchdog alarm and set it. This is used in case a mail check fails (e.g. we are
* killed by the system due to memory pressure.) Normally, a mail check will complete and
* the watchdog will be replaced by the call to reschedule().
* @param accountId the account we were trying to check
* @param alarmMgr system alarm manager
*/
private void setWatchdog(long accountId, AlarmManager alarmMgr) {
PendingIntent pi = createAlarmIntent(accountId, null, true);
long timeNow = SystemClock.elapsedRealtime();
long nextCheckTime = timeNow + WATCHDOG_DELAY;
alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi);
}
/**
* Return a pending intent for use by this alarm. Most of the fields must be the same
* (in order for the intent to be recognized by the alarm manager) but the extras can
* be different, and are passed in here as parameters.
*/
private PendingIntent createAlarmIntent(long checkId, long[] accountInfo, boolean isWatchdog) {
Intent i = new Intent();
i.setClass(this, MailService.class);
i.setAction(ACTION_CHECK_MAIL);
i.putExtra(EXTRA_ACCOUNT, checkId);
i.putExtra(EXTRA_ACCOUNT_INFO, accountInfo);
if (isWatchdog) {
i.putExtra(EXTRA_DEBUG_WATCHDOG, true);
}
PendingIntent pi = PendingIntent.getService(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);
return pi;
}
/**
* Start a controller sync for a specific account
*
* @param controller The controller to do the sync work
* @param checkAccountId the account Id to try and check
* @param startId the id of this service launch
* @return true if mail checking has started, false if it could not (e.g. bad account id)
*/
private boolean syncOneAccount(Controller controller, long checkAccountId, int startId) {
long inboxId = Mailbox.findMailboxOfType(this, checkAccountId, Mailbox.TYPE_INBOX);
if (inboxId == Mailbox.NO_MAILBOX) {
return false;
} else {
controller.serviceCheckMail(checkAccountId, inboxId, startId);
return true;
}
}
/**
* Note: Times are relative to SystemClock.elapsedRealtime()
*
* TODO: Look more closely at syncEnabled and see if we can simply coalesce it into
* syncInterval (e.g. if !syncEnabled, set syncInterval to -1).
*/
@VisibleForTesting
static class AccountSyncReport {
long accountId;
/** The time of the last sync, or, {@code 0}, the last sync time is unknown. */
long prevSyncTime;
/** The time of the next sync. If {@code 0}, sync ASAP. If {@code 1}, don't sync. */
long nextSyncTime;
/** Minimum time between syncs; in minutes. */
int syncInterval;
/** If {@code true}, auto sync is enabled. */
boolean syncEnabled;
/**
* Sets the next sync time using the previous sync time and sync interval.
*/
private void setNextSyncTime() {
if (syncInterval > 0 && prevSyncTime != 0) {
nextSyncTime = prevSyncTime + (syncInterval * 1000 * 60);
}
}
@Override
public String toString() {
return "id=" + accountId + " prevSync=" + prevSyncTime + " nextSync=" + nextSyncTime;
}
}
/**
* scan accounts to create a list of { acct, prev sync, next sync, #new }
* use this to create a fresh copy. assumes all accounts need sync
*
* @param accountId -1 will rebuild the list if empty. other values will force loading
* of a single account (e.g if it was created after the original list population)
*/
private void setupSyncReports(long accountId) {
synchronized (mSyncReports) {
setupSyncReportsLocked(accountId, mContext);
}
}
/**
* Handle the work of setupSyncReports. Must be synchronized on mSyncReports.
*/
@VisibleForTesting
void setupSyncReportsLocked(long accountId, Context context) {
ContentResolver resolver = context.getContentResolver();
if (accountId == SYNC_REPORTS_RESET) {
// For test purposes, force refresh of mSyncReports
mSyncReports.clear();
accountId = SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY;
} else if (accountId == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) {
// -1 == reload the list if empty, otherwise exit immediately
if (mSyncReports.size() > 0) {
return;
}
} else {
// load a single account if it doesn't already have a sync record
if (mSyncReports.containsKey(accountId)) {
return;
}
}
// setup to add a single account or all accounts
Uri uri;
if (accountId == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) {
uri = Account.CONTENT_URI;
} else {
uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
}
final boolean oneMinuteRefresh
= Preferences.getPreferences(this).getForceOneMinuteRefresh();
if (oneMinuteRefresh) {
Log.w(LOG_TAG, "One-minute refresh enabled.");
}
// We use a full projection here because we'll restore each account object from it
Cursor c = resolver.query(uri, Account.CONTENT_PROJECTION, null, null, null);
try {
while (c.moveToNext()) {
Account account = Account.getContent(c, Account.class);
// The following sanity checks are primarily for the sake of ignoring non-user
// accounts that may have been left behind e.g. by failed unit tests.
// Properly-formed accounts will always pass these simple checks.
if (TextUtils.isEmpty(account.mEmailAddress)
|| account.mHostAuthKeyRecv <= 0
|| account.mHostAuthKeySend <= 0) {
continue;
}
// The account is OK, so proceed
AccountSyncReport report = new AccountSyncReport();
int syncInterval = account.mSyncInterval;
// If we're not using MessagingController (EAS at this point), don't schedule syncs
if (!mController.isMessagingController(account.mId)) {
syncInterval = Account.CHECK_INTERVAL_NEVER;
} else if (oneMinuteRefresh && syncInterval >= 0) {
syncInterval = 1;
}
report.accountId = account.mId;
report.prevSyncTime = 0;
report.nextSyncTime = (syncInterval > 0) ? 0 : -1; // 0 == ASAP -1 == no sync
report.syncInterval = syncInterval;
// See if the account is enabled for sync in AccountManager
android.accounts.Account accountManagerAccount =
new android.accounts.Account(account.mEmailAddress,
AccountManagerTypes.TYPE_POP_IMAP);
report.syncEnabled = ContentResolver.getSyncAutomatically(accountManagerAccount,
EmailContent.AUTHORITY);
// TODO lookup # new in inbox
mSyncReports.put(report.accountId, report);
}
} finally {
c.close();
}
}
/**
* Update list with a single account's sync times and unread count
*
* @param accountId the account being updated
* @param newCount the number of new messages, or -1 if not being reported (don't update)
* @return the report for the updated account, or null if it doesn't exist (e.g. deleted)
*/
private AccountSyncReport updateAccountReport(long accountId, int newCount) {
// restore the reports if lost
setupSyncReports(accountId);
synchronized (mSyncReports) {
AccountSyncReport report = mSyncReports.get(accountId);
if (report == null) {
// discard result - there is no longer an account with this id
Log.d(LOG_TAG, "No account to update for id=" + Long.toString(accountId));
return null;
}
// report found - update it (note - editing the report while in-place in the hashmap)
report.prevSyncTime = SystemClock.elapsedRealtime();
report.setNextSyncTime();
if (Email.DEBUG) {
Log.d(LOG_TAG, "update account " + report.toString());
}
return report;
}
}
/**
* when we receive an alarm, update the account sync reports list if necessary
* this will be the case when if we have restarted the process and lost the data
* in the global.
*
* @param restoreIntent the intent with the list
*/
private void restoreSyncReports(Intent restoreIntent) {
// restore the reports if lost
setupSyncReports(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY);
synchronized (mSyncReports) {
long[] accountInfo = restoreIntent.getLongArrayExtra(EXTRA_ACCOUNT_INFO);
if (accountInfo == null) {
Log.d(LOG_TAG, "no data in intent to restore");
return;
}
int accountInfoIndex = 0;
int accountInfoLimit = accountInfo.length;
while (accountInfoIndex < accountInfoLimit) {
long accountId = accountInfo[accountInfoIndex++];
long prevSync = accountInfo[accountInfoIndex++];
AccountSyncReport report = mSyncReports.get(accountId);
if (report != null) {
if (report.prevSyncTime == 0) {
report.prevSyncTime = prevSync;
report.setNextSyncTime();
}
}
}
}
}
class ControllerResults extends Controller.Result {
@Override
public void updateMailboxCallback(MessagingException result, long accountId,
long mailboxId, int progress, int numNewMessages,
ArrayList<Long> addedMessages) {
// First, look for authentication failures and notify
//checkAuthenticationStatus(result, accountId);
if (result != null || progress == 100) {
// We only track the inbox here in the service - ignore other mailboxes
long inboxId = Mailbox.findMailboxOfType(MailService.this,
accountId, Mailbox.TYPE_INBOX);
if (mailboxId == inboxId) {
if (progress == 100) {
updateAccountReport(accountId, numNewMessages);
} else {
updateAccountReport(accountId, -1);
}
}
}
}
@Override
public void serviceCheckMailCallback(MessagingException result, long accountId,
long mailboxId, int progress, long tag) {
if (result != null || progress == 100) {
if (result != null) {
// the checkmail ended in an error. force an update of the refresh
// time, so we don't just spin on this account
updateAccountReport(accountId, -1);
}
AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
reschedule(alarmManager);
int serviceId = mStartId;
if (tag != 0) {
serviceId = (int) tag;
}
stopSelf(serviceId);
}
}
}
public class EmailSyncStatusObserver implements SyncStatusObserver {
@Override
public void onStatusChanged(int which) {
// We ignore the argument (we can only get called in one case - when settings change)
}
}
public static ArrayList<Account> getPopImapAccountList(Context context) {
ArrayList<Account> providerAccounts = new ArrayList<Account>();
Cursor c = context.getContentResolver().query(Account.CONTENT_URI, Account.ID_PROJECTION,

View File

@ -0,0 +1,722 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.email.service;
import android.app.Service;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.TrafficStats;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;
import com.android.email.Email;
import com.android.email.LegacyConversions;
import com.android.email.NotificationController;
import com.android.email.mail.Store;
import com.android.email.provider.Utilities;
import com.android.emailcommon.AccountManagerTypes;
import com.android.emailcommon.Logging;
import com.android.emailcommon.TrafficFlags;
import com.android.emailcommon.internet.MimeUtility;
import com.android.emailcommon.mail.AuthenticationFailedException;
import com.android.emailcommon.mail.FetchProfile;
import com.android.emailcommon.mail.Flag;
import com.android.emailcommon.mail.Folder;
import com.android.emailcommon.mail.Folder.FolderType;
import com.android.emailcommon.mail.Folder.MessageRetrievalListener;
import com.android.emailcommon.mail.Folder.OpenMode;
import com.android.emailcommon.mail.Message;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.mail.Part;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.MessageColumns;
import com.android.emailcommon.provider.EmailContent.SyncColumns;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.service.IEmailServiceCallback;
import com.android.emailcommon.utility.AttachmentUtilities;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
public class Pop3Service extends Service {
private static final String TAG = "Pop3Service";
private static final int MAX_SMALL_MESSAGE_SIZE = (25 * 1024);
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return Service.START_STICKY;
}
// Callbacks as set up via setCallback
private static final RemoteCallbackList<IEmailServiceCallback> mCallbackList =
new RemoteCallbackList<IEmailServiceCallback>();
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<IEmailServiceCallback> callbackList = mCallbackList;
if (callbackList != null) {
// Call everyone on our callback list
int count = callbackList.beginBroadcast();
try {
for (int i = 0; i < count; i++) {
try {
wrapper.call(callbackList.getBroadcastItem(i));
} catch (RemoteException e) {
// Safe to ignore
} catch (RuntimeException e) {
// We don't want an exception in one call to prevent other calls, so
// we'll just log this and continue
Log.e(TAG, "Caught RuntimeException in broadcast", e);
}
}
} finally {
// No matter what, we need to finish the broadcast
callbackList.finishBroadcast();
}
}
}
@Override
public void loadAttachmentStatus(final long messageId, final long attachmentId,
final int status, final int progress) {
broadcastCallback(new ServiceCallbackWrapper() {
@Override
public void call(IEmailServiceCallback cb) throws RemoteException {
cb.loadAttachmentStatus(messageId, attachmentId, status, progress);
}
});
}
@Override
public void loadMessageStatus(final long messageId, final int status, final int progress) {
broadcastCallback(new ServiceCallbackWrapper() {
@Override
public void call(IEmailServiceCallback cb) throws RemoteException {
cb.loadMessageStatus(messageId, status, progress);
}
});
}
@Override
public void sendMessageStatus(final long messageId, final String subject, final int status,
final int progress) {
broadcastCallback(new ServiceCallbackWrapper() {
@Override
public void call(IEmailServiceCallback cb) throws RemoteException {
cb.sendMessageStatus(messageId, subject, status, progress);
}
});
}
@Override
public void syncMailboxListStatus(final long accountId, final int status,
final int progress) {
broadcastCallback(new ServiceCallbackWrapper() {
@Override
public void call(IEmailServiceCallback cb) throws RemoteException {
cb.syncMailboxListStatus(accountId, status, progress);
}
});
}
@Override
public void syncMailboxStatus(final long mailboxId, final int status,
final int progress) {
broadcastCallback(new ServiceCallbackWrapper() {
@Override
public void call(IEmailServiceCallback cb) throws RemoteException {
cb.syncMailboxStatus(mailboxId, status, progress);
}
});
}
};
/**
* Create our EmailService implementation here.
*/
private final EmailServiceStub mBinder = new EmailServiceStub() {
@Override
public void startSync(long mailboxId, boolean userRequest) throws RemoteException {
Context context = getApplicationContext();
Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
if (mailbox == null) return;
Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey);
if (account == null) return;
android.accounts.Account acct = new android.accounts.Account(account.mEmailAddress,
AccountManagerTypes.TYPE_POP_IMAP);
Log.d(TAG, "startSync API requesting sync");
Bundle extras = new Bundle();
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
ContentResolver.requestSync(acct, EmailContent.AUTHORITY, extras);
}
@Override
public void setCallback(IEmailServiceCallback cb) throws RemoteException {
mCallbackList.register(cb);
}
};
@Override
public IBinder onBind(Intent intent) {
mBinder.init(this, sCallbackProxy);
return mBinder;
}
/**
* Start foreground synchronization of the specified folder. This is called by
* synchronizeMailbox or checkMail.
* TODO this should use ID's instead of fully-restored objects
* @param account
* @param folder
* @throws MessagingException
*/
public static void synchronizeMailboxSynchronous(Context context, final Account account,
final Mailbox folder) throws MessagingException {
TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account));
if ((folder.mFlags & Mailbox.FLAG_HOLDS_MAIL) == 0) {
}
NotificationController nc = NotificationController.getInstance(context);
try {
processPendingActionsSynchronous(context, account);
synchronizeMailboxGeneric(context, account, folder);
// Clear authentication notification for this account
nc.cancelLoginFailedNotification(account.mId);
} catch (MessagingException e) {
if (Logging.LOGD) {
Log.v(Logging.LOG_TAG, "synchronizeMailbox", e);
}
if (e instanceof AuthenticationFailedException) {
// Generate authentication notification
nc.showLoginFailedNotification(account.mId);
}
throw e;
}
}
/**
* Lightweight record for the first pass of message sync, where I'm just seeing if
* the local message requires sync. Later (for messages that need syncing) we'll do a full
* readout from the DB.
*/
private static class LocalMessageInfo {
private static final int COLUMN_ID = 0;
private static final int COLUMN_FLAG_READ = 1;
private static final int COLUMN_FLAG_FAVORITE = 2;
private static final int COLUMN_FLAG_LOADED = 3;
private static final int COLUMN_SERVER_ID = 4;
private static final int COLUMN_FLAGS = 7;
private static final String[] PROJECTION = new String[] {
EmailContent.RECORD_ID,
MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_LOADED,
SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY,
MessageColumns.FLAGS
};
final long mId;
final boolean mFlagRead;
final boolean mFlagFavorite;
final int mFlagLoaded;
final String mServerId;
final int mFlags;
public LocalMessageInfo(Cursor c) {
mId = c.getLong(COLUMN_ID);
mFlagRead = c.getInt(COLUMN_FLAG_READ) != 0;
mFlagFavorite = c.getInt(COLUMN_FLAG_FAVORITE) != 0;
mFlagLoaded = c.getInt(COLUMN_FLAG_LOADED);
mServerId = c.getString(COLUMN_SERVER_ID);
mFlags = c.getInt(COLUMN_FLAGS);
// Note: mailbox key and account key not needed - they are projected for the SELECT
}
}
private static void saveOrUpdate(EmailContent content, Context context) {
if (content.isSaved()) {
content.update(context, content.toContentValues());
} else {
content.save(context);
}
}
/**
* Load the structure and body of messages not yet synced
* @param account the account we're syncing
* @param remoteFolder the (open) Folder we're working on
* @param unsyncedMessages an array of Message's we've got headers for
* @param toMailbox the destination mailbox we're syncing
* @throws MessagingException
*/
static void loadUnsyncedMessages(final Context context, final Account account,
Folder remoteFolder, ArrayList<Message> 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<Message> largeMessages = new ArrayList<Message>();
ArrayList<Message> smallMessages = new ArrayList<Message>();
for (Message message : unsyncedMessages) {
if (message.getSize() > (MAX_SMALL_MESSAGE_SIZE)) {
largeMessages.add(message);
} else {
smallMessages.add(message);
}
}
// 2. Download small messages
// TODO Problems with this implementation. 1. For IMAP, where we get a real envelope,
// this is going to be inefficient and duplicate work we've already done. 2. It's going
// back to the DB for a local message that we already had (and discarded).
// For small messages, we specify "body", which returns everything (incl. attachments)
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.BODY);
remoteFolder.fetch(smallMessages.toArray(new Message[smallMessages.size()]), fp,
new MessageRetrievalListener() {
@Override
public void messageRetrieved(Message message) {
// Store the updated message locally and mark it fully loaded
Utilities.copyOneMessageToProvider(context, message, account, toMailbox,
EmailContent.Message.FLAG_LOADED_COMPLETE);
}
@Override
public void loadAttachmentProgress(int progress) {
}
});
// 3. Download large messages. We ask the server to give us the message structure,
// but not all of the attachments.
fp.clear();
fp.add(FetchProfile.Item.STRUCTURE);
remoteFolder.fetch(largeMessages.toArray(new Message[largeMessages.size()]), fp, null);
for (Message message : largeMessages) {
if (message.getBody() == null) {
// POP doesn't support STRUCTURE mode, so we'll just do a partial download
// (hopefully enough to see some/all of the body) and mark the message for
// further download.
fp.clear();
fp.add(FetchProfile.Item.BODY_SANE);
// TODO a good optimization here would be to make sure that all Stores set
// the proper size after this fetch and compare the before and after size. If
// they equal we can mark this SYNCHRONIZED instead of PARTIALLY_SYNCHRONIZED
remoteFolder.fetch(new Message[] { message }, fp, null);
// Store the partially-loaded message and mark it partially loaded
Utilities.copyOneMessageToProvider(context, message, account, toMailbox,
EmailContent.Message.FLAG_LOADED_PARTIAL);
} else {
// We have a structure to deal with, from which
// we can pull down the parts we want to actually store.
// Build a list of parts we are interested in. Text parts will be downloaded
// right now, attachments will be left for later.
ArrayList<Part> viewables = new ArrayList<Part>();
ArrayList<Part> attachments = new ArrayList<Part>();
MimeUtility.collectParts(message, viewables, attachments);
// Download the viewables immediately
for (Part part : viewables) {
fp.clear();
fp.add(part);
// TODO what happens if the network connection dies? We've got partial
// messages with incorrect status stored.
remoteFolder.fetch(new Message[] { message }, fp, null);
}
// Store the updated message locally and mark it fully loaded
Utilities.copyOneMessageToProvider(context, message, account, toMailbox,
EmailContent.Message.FLAG_LOADED_COMPLETE);
}
}
}
public static void downloadFlagAndEnvelope(final Context context, final Account account,
final Mailbox mailbox, Folder remoteFolder, ArrayList<Message> unsyncedMessages,
HashMap<String, LocalMessageInfo> localMessageMap, final ArrayList<Long> unseenMessages)
throws MessagingException {
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.FLAGS);
fp.add(FetchProfile.Item.ENVELOPE);
final HashMap<String, LocalMessageInfo> localMapCopy;
if (localMessageMap != null)
localMapCopy = new HashMap<String, LocalMessageInfo>(localMessageMap);
else {
localMapCopy = new HashMap<String, LocalMessageInfo>();
}
remoteFolder.fetch(unsyncedMessages.toArray(new Message[0]), fp,
new MessageRetrievalListener() {
@Override
public void messageRetrieved(Message message) {
try {
// Determine if the new message was already known (e.g. partial)
// And create or reload the full message info
LocalMessageInfo localMessageInfo =
localMapCopy.get(message.getUid());
EmailContent.Message localMessage = null;
if (localMessageInfo == null) {
localMessage = new EmailContent.Message();
} else {
localMessage = EmailContent.Message.restoreMessageWithId(
context, localMessageInfo.mId);
}
if (localMessage != null) {
try {
// Copy the fields that are available into the message
LegacyConversions.updateMessageFields(localMessage,
message, account.mId, mailbox.mId);
// Commit the message to the local store
saveOrUpdate(localMessage, context);
// Track the "new" ness of the downloaded message
if (!message.isSet(Flag.SEEN) && unseenMessages != null) {
unseenMessages.add(localMessage.mId);
}
} catch (MessagingException me) {
Log.e(Logging.LOG_TAG,
"Error while copying downloaded message." + me);
}
}
}
catch (Exception e) {
Log.e(Logging.LOG_TAG,
"Error while storing downloaded message." + e.toString());
}
}
@Override
public void loadAttachmentProgress(int progress) {
}
});
}
/**
* Synchronizer for IMAP.
*
* TODO Break this method up into smaller chunks.
*
* @param account the account to sync
* @param mailbox the mailbox to sync
* @return results of the sync pass
* @throws MessagingException
*/
private static void synchronizeMailboxGeneric(final Context context,
final Account account, final Mailbox mailbox) throws MessagingException {
/*
* A list of IDs for messages that were downloaded and did not have the seen flag set.
* This serves as the "true" new message count reported to the user via notification.
*/
final ArrayList<Long> unseenMessages = new ArrayList<Long>();
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, "*** synchronizeMailboxGeneric ***");
}
ContentResolver resolver = context.getContentResolver();
// 0. We do not ever sync DRAFTS or OUTBOX (down or up)
if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) {
return;
}
// 1. Get the message list from the local store and create an index of the uids
Cursor localUidCursor = null;
HashMap<String, LocalMessageInfo> localMessageMap = new HashMap<String, LocalMessageInfo>();
try {
localUidCursor = resolver.query(
EmailContent.Message.CONTENT_URI,
LocalMessageInfo.PROJECTION,
EmailContent.MessageColumns.ACCOUNT_KEY + "=?" +
" AND " + MessageColumns.MAILBOX_KEY + "=?",
new String[] {
String.valueOf(account.mId),
String.valueOf(mailbox.mId)
},
null);
while (localUidCursor.moveToNext()) {
LocalMessageInfo info = new LocalMessageInfo(localUidCursor);
localMessageMap.put(info.mServerId, info);
}
} finally {
if (localUidCursor != null) {
localUidCursor.close();
}
}
// 2. Open the remote folder and create the remote folder if necessary
Store remoteStore = Store.getInstance(account, context);
// The account might have been deleted
if (remoteStore == null) return;
Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
/*
* If the folder is a "special" folder we need to see if it exists
* on the remote server. It if does not exist we'll try to create it. If we
* can't create we'll abort. This will happen on every single Pop3 folder as
* designed and on Imap folders during error conditions. This allows us
* to treat Pop3 and Imap the same in this code.
*/
if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_SENT
|| mailbox.mType == Mailbox.TYPE_DRAFTS) {
if (!remoteFolder.exists()) {
if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) {
return;
}
}
}
// 3, Open the remote folder. This pre-loads certain metadata like message count.
remoteFolder.open(OpenMode.READ_WRITE);
// 4. Trash any remote messages that are marked as trashed locally.
// TODO - this comment was here, but no code was here.
// 5. Get the remote message count.
int remoteMessageCount = remoteFolder.getMessageCount();
// 6. Determine the limit # of messages to download
int visibleLimit = mailbox.mVisibleLimit;
if (visibleLimit <= 0) {
visibleLimit = Email.VISIBLE_LIMIT_DEFAULT;
}
// 7. Create a list of messages to download
Message[] remoteMessages = new Message[0];
final ArrayList<Message> unsyncedMessages = new ArrayList<Message>();
HashMap<String, Message> remoteUidMap = new HashMap<String, Message>();
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<String> localUidsToDelete = new HashSet<String>(localMessageMap.keySet());
localUidsToDelete.removeAll(remoteUidMap.keySet());
for (String uidToDelete : localUidsToDelete) {
LocalMessageInfo infoToDelete = localMessageMap.get(uidToDelete);
// Delete associated data (attachment files)
// Attachment & Body records are auto-deleted when we delete the Message record
AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId,
infoToDelete.mId);
// Delete the message itself
Uri uriToDelete = ContentUris.withAppendedId(
EmailContent.Message.CONTENT_URI, infoToDelete.mId);
resolver.delete(uriToDelete, null, null);
// Delete extra rows (e.g. synced or deleted)
Uri syncRowToDelete = ContentUris.withAppendedId(
EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId);
resolver.delete(syncRowToDelete, null, null);
Uri deletERowToDelete = ContentUris.withAppendedId(
EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId);
resolver.delete(deletERowToDelete, null, null);
}
loadUnsyncedMessages(context, account, remoteFolder, unsyncedMessages, mailbox);
// 14. Clean up and report results
remoteFolder.close(false);
}
/**
* Find messages in the updated table that need to be written back to server.
*
* Handles:
* Read/Unread
* Flagged
* Append (upload)
* Move To Trash
* Empty trash
* TODO:
* Move
*
* @param account the account to scan for pending actions
* @throws MessagingException
*/
private static void processPendingActionsSynchronous(Context context, Account account)
throws MessagingException {
TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account));
String[] accountIdArgs = new String[] { Long.toString(account.mId) };
// Handle deletes first, it's always better to get rid of things first
processPendingDeletesSynchronous(context, account, accountIdArgs);
}
/**
* Scan for messages that are in the Message_Deletes table, look for differences that
* we can deal with, and do the work.
*
* @param account
* @param resolver
* @param accountIdArgs
*/
private static void processPendingDeletesSynchronous(Context context, Account account,
String[] accountIdArgs) {
Cursor deletes = context.getContentResolver().query(
EmailContent.Message.DELETED_CONTENT_URI,
EmailContent.Message.CONTENT_PROJECTION,
EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs,
EmailContent.MessageColumns.MAILBOX_KEY);
try {
// loop through messages marked as deleted
while (deletes.moveToNext()) {
EmailContent.Message oldMessage =
EmailContent.getContent(deletes, EmailContent.Message.class);
// Finally, delete the update
Uri uri = ContentUris.withAppendedId(EmailContent.Message.DELETED_CONTENT_URI,
oldMessage.mId);
context.getContentResolver().delete(uri, null, null);
}
} finally {
deletes.close();
}
}
}

View File

@ -35,6 +35,7 @@ import com.android.emailcommon.provider.EmailContent.AccountColumns;
import com.android.emailcommon.provider.EmailContent.Message;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.service.EmailServiceProxy;
import java.util.ArrayList;
@ -88,7 +89,11 @@ public class PopImapSyncAdapterService extends Service {
Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey);
if (account == null) return;
try {
ImapService.synchronizeMailboxSynchronous(context, account, mailbox);
if (account.getProtocol(context).equals(HostAuth.SCHEME_IMAP)) {
ImapService.synchronizeMailboxSynchronous(context, account, mailbox);
} else {
Pop3Service.synchronizeMailboxSynchronous(context, account, mailbox);
}
} catch (MessagingException e) {
int cause = e.getExceptionType();
switch(cause) {
@ -117,52 +122,55 @@ public class PopImapSyncAdapterService extends Service {
if (c != null && c.moveToNext()) {
Account acct = new Account();
acct.restore(c);
String protocol = acct.getProtocol(context);
if (protocol.equals(HostAuth.SCHEME_IMAP)) {
if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD)) {
Log.d(TAG, "Upload sync request for " + acct.mDisplayName);
// See if any boxes have mail...
Cursor updatesCursor = provider.query(Message.UPDATED_CONTENT_URI,
new String[] {Message.MAILBOX_KEY},
Message.ACCOUNT_KEY + "=?",
new String[] {Long.toString(acct.mId)},
null);
if ((updatesCursor == null) || (updatesCursor.getCount() == 0)) return;
ArrayList<Long> mailboxesToUpdate = new ArrayList<Long>();
while (updatesCursor.moveToNext()) {
Long mailboxId = updatesCursor.getLong(0);
if (!mailboxesToUpdate.contains(mailboxId)) {
mailboxesToUpdate.add(mailboxId);
}
if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD)) {
Log.d(TAG, "Upload sync request for " + acct.mDisplayName);
// See if any boxes have mail...
Cursor updatesCursor = provider.query(Message.UPDATED_CONTENT_URI,
new String[] {Message.MAILBOX_KEY},
Message.ACCOUNT_KEY + "=?",
new String[] {Long.toString(acct.mId)},
null);
if ((updatesCursor == null) || (updatesCursor.getCount() == 0)) return;
ArrayList<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);
}
for (long mailboxId: mailboxesToUpdate) {
sync(context, mailboxId, syncResult);
}
} else {
Log.d(TAG, "Sync request for " + acct.mDisplayName);
Log.d(TAG, extras.toString());
long mailboxId = extras.getLong("MAILBOX_ID", Mailbox.NO_MAILBOX);
boolean isInbox = false;
if (mailboxId == Mailbox.NO_MAILBOX) {
mailboxId = Mailbox.findMailboxOfType(context, acct.mId,
Mailbox.TYPE_INBOX);
if (mailboxId == Mailbox.NO_MAILBOX) {
// Update folders?
EmailServiceProxy service =
EmailServiceUtils.getServiceForAccount(context, null, acct.mId);
service.updateFolderList(acct.mId);
}
isInbox = true;
}
if (mailboxId == Mailbox.NO_MAILBOX) return;
sync(context, mailboxId, syncResult);
// Convert from minutes to seconds
int syncFrequency = acct.mSyncInterval * 60;
// Values < 0 are for "never" or "push"; 0 is undefined
if (syncFrequency <= 0) return;
Bundle ex = new Bundle();
if (!isInbox) {
ex.putLong("MAILBOX_ID", mailboxId);
}
Log.d(TAG, "Setting periodic sync for " + acct.mDisplayName + ": " +
syncFrequency + " seconds");
ContentResolver.addPeriodicSync(account, authority, ex, syncFrequency);
}
}
} catch (Exception e) {

View File

@ -64,12 +64,6 @@ public class ControllerProviderOpsTests extends ProviderTestCase2<EmailProvider>
ContentCache.invalidateAllCaches();
}
@Override
public void tearDown() throws Exception {
super.tearDown();
mTestController.cleanupForTest();
}
/**
* Lightweight subclass of the Controller class allows injection of mock context
*/
@ -488,44 +482,6 @@ public class ControllerProviderOpsTests extends ProviderTestCase2<EmailProvider>
return hostAuth;
}
public void testIsMessagingController() {
Account account1 = ProviderTestUtils.setupAccount("account1", false,
mProviderContext);
account1.mHostAuthRecv = setupSimpleHostAuth("eas");
account1.save(mProviderContext);
assertFalse(mTestController.isMessagingController(account1));
Account account2 = ProviderTestUtils.setupAccount("account2", false,
mProviderContext);
account2.mHostAuthRecv = setupSimpleHostAuth("imap");
account2.save(mProviderContext);
assertTrue(mTestController.isMessagingController(account2));
Account account3 = ProviderTestUtils.setupAccount("account3", false,
mProviderContext);
account3.mHostAuthRecv = setupSimpleHostAuth("pop3");
account3.save(mProviderContext);
assertTrue(mTestController.isMessagingController(account3));
Account account4 = ProviderTestUtils.setupAccount("account4", false,
mProviderContext);
account4.mHostAuthRecv = setupSimpleHostAuth("smtp");
account4.save(mProviderContext);
assertFalse(mTestController.isMessagingController(account4));
// There should be values for all of these accounts in the legacy map
assertNotNull(mTestController.mLegacyControllerMap.get(account1.mId));
assertNotNull(mTestController.mLegacyControllerMap.get(account2.mId));
assertNotNull(mTestController.mLegacyControllerMap.get(account3.mId));
assertNotNull(mTestController.mLegacyControllerMap.get(account4.mId));
// The map should have the expected values
assertFalse(mTestController.mLegacyControllerMap.get(account1.mId));
assertTrue(mTestController.mLegacyControllerMap.get(account2.mId));
assertTrue(mTestController.mLegacyControllerMap.get(account3.mId));
assertFalse(mTestController.mLegacyControllerMap.get(account4.mId));
// This second pass should pull values from the cache
assertFalse(mTestController.isMessagingController(account1));
assertTrue(mTestController.isMessagingController(account2));
assertTrue(mTestController.isMessagingController(account3));
assertFalse(mTestController.isMessagingController(account4));
}
/**
* TODO: releasing associated data (e.g. attachments, embedded images)
*/

View File

@ -1,85 +0,0 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.email;
import com.android.emailcommon.mail.MockFolder;
import com.android.emailcommon.provider.Account;
import android.content.ContentUris;
import android.net.Uri;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
/**
* This is a series of unit tests for the MessagingController class.
*
* Technically these are functional because they use the underlying provider framework.
*/
@SmallTest
public class MessagingControllerUnitTests extends AndroidTestCase {
private long mAccountId;
private Account mAccount;
/**
* Delete any dummy accounts we set up for this test
*/
@Override
protected void tearDown() throws Exception {
super.tearDown();
if (mAccount != null) {
Uri uri = ContentUris.withAppendedId(
Account.CONTENT_URI, mAccountId);
getContext().getContentResolver().delete(uri, null, null);
}
}
/**
* MockFolder allows setting and retrieving role & name
*/
private static class MyMockFolder extends MockFolder {
private FolderRole mRole;
private String mName;
public MyMockFolder(FolderRole role, String name) {
mRole = role;
mName = name;
}
@Override
public String getName() {
return mName;
}
@Override
public FolderRole getRole() {
return mRole;
}
}
/**
* Create a dummy account with minimal fields
*/
private void createTestAccount() {
mAccount = new Account();
mAccount.save(getContext());
mAccountId = mAccount.mId;
}
}

View File

@ -62,12 +62,6 @@ public class RefreshManagerTest extends InstrumentationTestCase {
mTarget.registerListener(mListener);
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
mController.cleanupForTest();
}
public void testRegisterUnregisterListener() {
// mListener is already registered
assertEquals(1, mTarget.getListenersForTest().size());

View File

@ -77,7 +77,6 @@ public class MailboxFinderTest extends InstrumentationTestCase {
// MailboxFinder should unregister its listener when closed.
checkControllerResultRemoved(mMockController);
}
mMockController.cleanupForTest();
Controller.injectMockControllerForTest(null);
}

View File

@ -24,7 +24,6 @@ import com.android.email.EmailConnectivityManager;
import com.android.email.provider.ProviderTestUtils;
import com.android.email.service.AttachmentDownloadService.DownloadRequest;
import com.android.email.service.AttachmentDownloadService.DownloadSet;
import com.android.email.service.EmailServiceUtils.NullEmailService;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent.Attachment;
import com.android.emailcommon.provider.EmailContent.Message;
@ -71,7 +70,7 @@ public class AttachmentDownloadServiceTests extends AccountTestCase {
mService = new AttachmentDownloadService();
mService.mContext = mMockContext;
mService.addServiceIntentForTest(mAccountId, new Intent(mContext,
NullEmailService.class));
EmailServiceStub.class));
mAccountManagerStub = new AttachmentDownloadService.AccountManagerStub(null);
mService.mAccountManagerStub = mAccountManagerStub;
mService.mConnectivityManager = new MockConnectivityManager(mContext, "mock");
@ -183,10 +182,12 @@ public class AttachmentDownloadServiceTests extends AccountTestCase {
mUsableSpace = usable;
}
@Override
public long getTotalSpace() {
return mTotalSpace;
}
@Override
public long getUsableSpace() {
return mUsableSpace;
}
@ -195,6 +196,7 @@ public class AttachmentDownloadServiceTests extends AccountTestCase {
mMockFile.mLength = length;
}
@Override
public File[] listFiles() {
return mFiles;
}
@ -211,6 +213,7 @@ public class AttachmentDownloadServiceTests extends AccountTestCase {
super("_mock");
}
@Override
public long length() {
return mLength;
}

View File

@ -1,331 +0,0 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.email.service;
import android.accounts.AccountManager;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageManager;
import com.android.email.AccountTestCase;
import com.android.email.Controller;
import com.android.email.provider.AccountReconciler;
import com.android.email.provider.EmailProvider;
import com.android.email.provider.ProviderTestUtils;
import com.android.email.service.MailService.AccountSyncReport;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.HostAuth;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* Tests of the Email provider.
*
* You can run this entire test case with:
* runtest -c com.android.email.service.MailServiceTests email
*/
public class MailServiceTests extends AccountTestCase {
EmailProvider mProvider;
Context mMockContext;
public MailServiceTests() {
super();
}
@Override
public void setUp() throws Exception {
super.setUp();
PackageManager pm = getContext().getPackageManager();
pm.setComponentEnabledSetting(
new ComponentName(getContext(), EasTestAuthenticatorService.class),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP);
mMockContext = getMockContext();
// Delete any test accounts we might have created earlier
deleteTemporaryAccountManagerAccounts();
}
@Override
public void tearDown() throws Exception {
super.tearDown();
// Delete any test accounts we might have created earlier
deleteTemporaryAccountManagerAccounts();
}
/**
* Confirm that the test below is functional (and non-destructive) when there are
* prexisting (non-test) accounts in the account manager.
*/
public void testTestReconcileAccounts() {
Account firstAccount = null;
final String TEST_USER_ACCOUNT = "__user_account_test_1";
Context context = getContext();
try {
// Note: Unlike calls to setupProviderAndAccountManagerAccount(), we are creating
// *real* accounts here (not in the mock provider)
createAccountManagerAccount(TEST_USER_ACCOUNT + TEST_ACCOUNT_SUFFIX);
firstAccount = ProviderTestUtils.setupAccount(TEST_USER_ACCOUNT, true, context);
// Now run the test with the "user" accounts in place
testReconcileAccounts();
} finally {
if (firstAccount != null) {
boolean firstAccountFound = false;
// delete the provider account
context.getContentResolver().delete(firstAccount.getUri(), null, null);
// delete the account manager account
android.accounts.Account[] accountManagerAccounts = AccountManager.get(context)
.getAccountsByType(TEST_ACCOUNT_TYPE);
for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
if ((TEST_USER_ACCOUNT + TEST_ACCOUNT_SUFFIX)
.equals(accountManagerAccount.name)) {
deleteAccountManagerAccount(accountManagerAccount);
firstAccountFound = true;
}
}
assertTrue(firstAccountFound);
}
}
}
/**
* Note, there is some inherent risk in this test, as it creates *real* accounts in the
* system (it cannot use the mock context with the Account Manager).
*/
public void testReconcileAccounts() {
// Note that we can't use mMockContext for AccountManager interactions, as it isn't a fully
// functional Context.
Context context = getContext();
// Capture the baseline (account manager accounts) so we can measure the changes
// we're making, irrespective of the number of actual accounts, and not destroy them
android.accounts.Account[] baselineAccounts =
AccountManager.get(context).getAccountsByType(TEST_ACCOUNT_TYPE);
// Set up three accounts, both in AccountManager and in EmailProvider
Account firstAccount = setupProviderAndAccountManagerAccount(getTestAccountName("1"));
setupProviderAndAccountManagerAccount(getTestAccountName("2"));
setupProviderAndAccountManagerAccount(getTestAccountName("3"));
// Check that they're set up properly
assertEquals(3, EmailContent.count(mMockContext, Account.CONTENT_URI, null, null));
android.accounts.Account[] accountManagerAccounts =
getAccountManagerAccounts(baselineAccounts);
assertEquals(3, accountManagerAccounts.length);
// Delete account "2" from AccountManager
android.accounts.Account removedAccount =
makeAccountManagerAccount(getTestAccountEmailAddress("2"));
deleteAccountManagerAccount(removedAccount);
// Confirm it's deleted
accountManagerAccounts = getAccountManagerAccounts(baselineAccounts);
assertEquals(2, accountManagerAccounts.length);
// Run the reconciler
ContentResolver resolver = mMockContext.getContentResolver();
MailService.reconcileAccountsWithAccountManager(context,
makeExchangeServiceAccountList(), accountManagerAccounts, mMockContext);
// There should now be only two EmailProvider accounts
assertEquals(2, EmailContent.count(mMockContext, Account.CONTENT_URI, null, null));
// Ok, now we've got two of each; let's delete a provider account
resolver.delete(ContentUris.withAppendedId(Account.CONTENT_URI, firstAccount.mId),
null, null);
// ...and then there was one
assertEquals(1, EmailContent.count(mMockContext, Account.CONTENT_URI, null, null));
// Run the reconciler
MailService.reconcileAccountsWithAccountManager(context,
makeExchangeServiceAccountList(), accountManagerAccounts, mMockContext);
// There should now be only one AccountManager account
accountManagerAccounts = getAccountManagerAccounts(baselineAccounts);
assertEquals(1, accountManagerAccounts.length);
// ... and it should be account "3"
assertEquals(getTestAccountEmailAddress("3"), accountManagerAccounts[0].name);
}
public void testReconcileDetection() {
Context context = getContext();
List<Account> providerAccounts;
android.accounts.Account[] accountManagerAccounts;
android.accounts.Account[] baselineAccounts =
AccountManager.get(context).getAccountsByType(TEST_ACCOUNT_TYPE);
// Empty lists match.
providerAccounts = new ArrayList<Account>();
accountManagerAccounts = new android.accounts.Account[0];
assertFalse(AccountReconciler.accountsNeedReconciling(
context, providerAccounts, accountManagerAccounts));
setupProviderAndAccountManagerAccount(getTestAccountName("1"));
accountManagerAccounts = getAccountManagerAccounts(baselineAccounts);
providerAccounts = makeExchangeServiceAccountList();
// A single account, but empty list on the other side is detected as needing reconciliation
assertTrue(AccountReconciler.accountsNeedReconciling(
context, new ArrayList<Account>(), accountManagerAccounts));
assertTrue(AccountReconciler.accountsNeedReconciling(
context, providerAccounts, new android.accounts.Account[0]));
// Note that no reconciliation should have happened though - we just wanted to detect it.
assertEquals(1, makeExchangeServiceAccountList().size());
assertEquals(1, getAccountManagerAccounts(baselineAccounts).length);
// Single account matches - no reconciliation should be detected.
assertFalse(AccountReconciler.accountsNeedReconciling(
context, providerAccounts, accountManagerAccounts));
// Provider: 1,2,3. AccountManager: 1, 3.
String username = getTestAccountName("2");
ProviderTestUtils.setupAccount(getTestAccountName("2"), true, getMockContext());
setupProviderAndAccountManagerAccount(getTestAccountName("3"));
accountManagerAccounts = getAccountManagerAccounts(baselineAccounts);
providerAccounts = makeExchangeServiceAccountList();
assertTrue(AccountReconciler.accountsNeedReconciling(
context, providerAccounts, accountManagerAccounts));
}
/**
* Lightweight subclass of the Controller class allows injection of mock context
*/
public static class TestController extends Controller {
protected TestController(Context providerContext, Context systemContext) {
super(systemContext);
setProviderContext(providerContext);
}
}
/**
* Create a simple HostAuth with protocol
*/
private HostAuth setupSimpleHostAuth(String protocol) {
HostAuth hostAuth = new HostAuth();
hostAuth.mProtocol = protocol;
return hostAuth;
}
/**
* Initial testing on setupSyncReportsLocked, making sure that EAS accounts aren't scheduled
*/
public void testSetupSyncReportsLocked() {
// TODO Test other functionality within setupSyncReportsLocked
// Setup accounts of each type, all with manual sync at different intervals
Account easAccount = ProviderTestUtils.setupAccount("account1", false, mMockContext);
easAccount.mHostAuthRecv = setupSimpleHostAuth("eas");
easAccount.mHostAuthSend = easAccount.mHostAuthRecv;
easAccount.mSyncInterval = 30;
easAccount.save(mMockContext);
Account imapAccount = ProviderTestUtils.setupAccount("account2", false, mMockContext);
imapAccount.mHostAuthRecv = setupSimpleHostAuth("imap");
imapAccount.mHostAuthSend = setupSimpleHostAuth("smtp");
imapAccount.mSyncInterval = 60;
imapAccount.save(mMockContext);
Account pop3Account = ProviderTestUtils.setupAccount("account3", false, mMockContext);
pop3Account.mHostAuthRecv = setupSimpleHostAuth("pop3");
pop3Account.mHostAuthSend = setupSimpleHostAuth("smtp");
pop3Account.mSyncInterval = 90;
pop3Account.save(mMockContext);
// Setup the SyncReport's for these Accounts
MailService mailService = new MailService();
mailService.mController = new TestController(mMockContext, getContext());
try {
mailService.setupSyncReportsLocked(MailService.SYNC_REPORTS_RESET, mMockContext);
// Get back the map created by MailService
HashMap<Long, AccountSyncReport> syncReportMap = MailService.mSyncReports;
synchronized (syncReportMap) {
// Check the SyncReport's for correctness of sync interval
AccountSyncReport syncReport = syncReportMap.get(easAccount.mId);
assertNotNull(syncReport);
// EAS sync interval should have been changed to "never"
assertEquals(Account.CHECK_INTERVAL_NEVER, syncReport.syncInterval);
syncReport = syncReportMap.get(imapAccount.mId);
assertNotNull(syncReport);
assertEquals(60, syncReport.syncInterval);
syncReport = syncReportMap.get(pop3Account.mId);
assertNotNull(syncReport);
assertEquals(90, syncReport.syncInterval);
// Change the EAS account to push
ContentValues cv = new ContentValues();
cv.put(Account.SYNC_INTERVAL, Account.CHECK_INTERVAL_PUSH);
easAccount.update(mMockContext, cv);
syncReportMap.clear();
mailService.setupSyncReportsLocked(easAccount.mId, mMockContext);
syncReport = syncReportMap.get(easAccount.mId);
assertNotNull(syncReport);
// EAS sync interval should be "never" in this case as well
assertEquals(Account.CHECK_INTERVAL_NEVER, syncReport.syncInterval);
}
} finally {
mailService.mController.cleanupForTest();
}
}
/**
* Test that setupSyncReports will skip over poorly-formed accounts which can be left
* over after unit tests.
*/
public void testSetupSyncReportsWithBadAccounts() {
// Setup accounts that trigger each skip-over case
// 1: no email address
Account account1 = ProviderTestUtils.setupAccount("account1", false, mMockContext);
account1.mHostAuthRecv = setupSimpleHostAuth("imap");
account1.mHostAuthSend = setupSimpleHostAuth("smtp");
account1.mSyncInterval = 30;
account1.mEmailAddress = null;
account1.save(mMockContext);
// 2: no receiver hostauth
Account account2 = ProviderTestUtils.setupAccount("account2", false, mMockContext);
account2.mHostAuthRecv = null;
account2.mHostAuthSend = setupSimpleHostAuth("smtp");
account2.mSyncInterval = 30;
account2.save(mMockContext);
// 3: no sender hostauth
Account account3 = ProviderTestUtils.setupAccount("account3", false, mMockContext);
account3.mHostAuthRecv = setupSimpleHostAuth("imap");
account3.mHostAuthSend = null;
account3.mSyncInterval = 30;
account3.save(mMockContext);
// Setup the SyncReport's for these Accounts
MailService mailService = new MailService();
mailService.mController = new TestController(mMockContext, getContext());
try {
mailService.setupSyncReportsLocked(MailService.SYNC_REPORTS_RESET, mMockContext);
// Get back the map created by MailService - it should be empty
HashMap<Long, AccountSyncReport> syncReportMap = MailService.mSyncReports;
assertEquals(0, syncReportMap.size());
} finally {
mailService.mController.cleanupForTest();
}
}
}