From 46d7d7f1b6387d144c3f9e7c987418dc8f55fad4 Mon Sep 17 00:00:00 2001 From: Andrew Stadler Date: Tue, 18 Aug 2009 00:54:34 -0700 Subject: [PATCH] Rework service to use provider accounts and controller. * Rewrite service logic to select and update one account at a time * Add checkmail API to Controller, and much rework/cleanup of existing callback API's * Rewrite notification posting code * Rewire connection to MessageList to be opened by notifications, to cancel notifications, and to reset the "new message" count whenever an account is viewed. * Boilerplate cleanup to a lot of activities because they share the callbacks that have had minor changes. * Remove old push controls from Store API In progress: * To provide notification mechanism for EAS pushed mail --- src/com/android/email/Controller.java | 151 +++-- .../android/email/GroupMessagingListener.java | 15 +- .../android/email/MessagingController.java | 116 +--- src/com/android/email/MessagingListener.java | 7 +- .../email/activity/AccountFolderList.java | 21 +- .../email/activity/FolderMessageList.java | 22 +- .../android/email/activity/MailboxList.java | 15 +- .../email/activity/MessageCompose.java | 12 +- .../android/email/activity/MessageList.java | 53 +- .../android/email/activity/MessageView.java | 9 +- src/com/android/email/mail/Store.java | 21 - .../mail/exchange/ExchangeStoreExample.java | 27 +- .../email/mail/store/ExchangeStore.java | 26 - .../android/email/service/MailService.java | 605 ++++++++++++------ .../exchange/adapter/EmailSyncAdapter.java | 2 +- 15 files changed, 623 insertions(+), 479 deletions(-) diff --git a/src/com/android/email/Controller.java b/src/com/android/email/Controller.java index 878d217a4..37be87fd5 100644 --- a/src/com/android/email/Controller.java +++ b/src/com/android/email/Controller.java @@ -53,6 +53,7 @@ public class Controller { private Context mContext; private Context mProviderContext; private MessagingController mLegacyController; + private LegacyListener mLegacyListener = new LegacyListener(); private ServiceCallback mServiceCallback = new ServiceCallback(); private HashSet mListeners = new HashSet(); @@ -66,6 +67,7 @@ public class Controller { mContext = _context; mProviderContext = _context; mLegacyController = MessagingController.getInstance(mContext); + mLegacyController.addListener(mLegacyListener); } /** @@ -158,11 +160,39 @@ public class Controller { new Thread() { @Override public void run() { - Account account = - EmailContent.Account.restoreAccountWithId(mProviderContext, accountId); - MessagingListener listener = new LegacyListener(callback); - mLegacyController.addListener(listener); - mLegacyController.listFolders(account, listener); + mLegacyController.listFolders(accountId, mLegacyListener); + } + }.start(); + } + } + + /** + * 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. + */ + public void serviceCheckMail(final long accountId, final long mailboxId, final long tag, + final Result callback) { + IEmailService service = getServiceForAccount(accountId); + if (service != null) { + // Service implementation +// try { + // TODO this isn't quite going to work, because we're going to get the + // generic (UI) callbacks and not the ones we need to restart the ol' service. + // service.startSync(mailboxId, tag); + callback.serviceCheckMailCallback(null, accountId, mailboxId, 100, tag); +// } catch (RemoteException e) { + // TODO Change exception handling to be consistent with however this method + // is implemented for other protocols +// Log.d("updateMailbox", "RemoteException" + e); +// } + } else { + // MessagingController implementation + new Thread() { + @Override + public void run() { + mLegacyController.checkMail(accountId, tag, mLegacyListener); } }.start(); } @@ -175,14 +205,13 @@ 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. */ - public void updateMailbox(final long accountId, - final EmailContent.Mailbox mailbox, final Result callback) { + public void updateMailbox(final long accountId, final long mailboxId, final Result callback) { IEmailService service = getServiceForAccount(accountId); if (service != null) { // Service implementation try { - service.startSync(mailbox.mId); + service.startSync(mailboxId); } catch (RemoteException e) { // TODO Change exception handling to be consistent with however this method // is implemented for other protocols @@ -193,11 +222,12 @@ public class Controller { new Thread() { @Override public void run() { + // TODO shouldn't be passing fully-build accounts & mailboxes into APIs Account account = EmailContent.Account.restoreAccountWithId(mProviderContext, accountId); - MessagingListener listener = new LegacyListener(callback); - mLegacyController.addListener(listener); - mLegacyController.synchronizeMailbox(account, mailbox, listener); + Mailbox mailbox = + EmailContent.Mailbox.restoreMailboxWithId(mProviderContext, mailboxId); + mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener); } }.start(); } @@ -516,7 +546,6 @@ public class Controller { * made from the UI thread, so you may need further handlers to safely make UI updates. */ public interface Result { - /** * Callback for updateMailboxList * @@ -528,15 +557,17 @@ public class Controller { int progress); /** - * Callback for updateMailbox + * Callback for updateMailbox. Note: This looks a lot like checkMailCallback, but + * it's a separate call used only by UI's, so we can keep things separate. * * @param result If null, the operation completed without error * @param accountId The account being operated on * @param mailboxId The mailbox being operated on * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete + * @param numNewMessages the number of new messages delivered */ public void updateMailboxCallback(MessagingException result, long accountId, - long mailboxId, int progress, int totalMessagesInMailbox, int numNewMessages); + long mailboxId, int progress, int numNewMessages); /** * Callback for loadAttachment @@ -548,6 +579,20 @@ public class Controller { */ public void loadAttachmentCallback(MessagingException result, long messageId, long attachmentId, int progress); + + /** + * Callback for checkmail. Note: This looks a lot like updateMailboxCallback, but + * it's a separate call used only by the automatic checker service, so we can keep + * things separate. + * + * @param result If null, the operation completed without error + * @param accountId The account being operated on + * @param mailboxId The mailbox being operated on (may be unknown at start) + * @param progress 0 for "starting", no updates, 100 for complete + * @param tag the same tag that was passed to serviceCheckMail() + */ + public void serviceCheckMailCallback(MessagingException result, long accountId, + long mailboxId, int progress, long tag); } /** @@ -555,70 +600,87 @@ public class Controller { * out of scope. */ private class LegacyListener extends MessagingListener { - Result mResultCallback; - - public LegacyListener(Result callback) { - mResultCallback = callback; - } @Override public void listFoldersStarted(EmailContent.Account account) { - if (mResultCallback != null && isActiveResultCallback(mResultCallback)) { - mResultCallback.updateMailboxListCallback(null, account.mId, 0); + synchronized (mListeners) { + for (Result l : mListeners) { + l.updateMailboxListCallback(null, account.mId, 0); + } } } @Override public void listFoldersFailed(EmailContent.Account account, String message) { - if (mResultCallback != null && isActiveResultCallback(mResultCallback)) { - mResultCallback.updateMailboxListCallback(new MessagingException(message), - account.mId, 0); + synchronized (mListeners) { + for (Result l : mListeners) { + l.updateMailboxListCallback(new MessagingException(message), account.mId, 0); + } } - mLegacyController.removeListener(this); } @Override public void listFoldersFinished(EmailContent.Account account) { - if (mResultCallback != null && isActiveResultCallback(mResultCallback)) { - mResultCallback.updateMailboxListCallback(null, account.mId, 100); + synchronized (mListeners) { + for (Result l : mListeners) { + l.updateMailboxListCallback(null, account.mId, 100); + } } - mLegacyController.removeListener(this); } @Override public void synchronizeMailboxStarted(EmailContent.Account account, EmailContent.Mailbox folder) { - if (mResultCallback != null && isActiveResultCallback(mResultCallback)) { - mResultCallback.updateMailboxCallback(null, account.mId, folder.mId, 0, -1, -1); + synchronized (mListeners) { + for (Result l : mListeners) { + l.updateMailboxCallback(null, account.mId, folder.mId, 0, 0); + } } } @Override public void synchronizeMailboxFinished(EmailContent.Account account, EmailContent.Mailbox folder, int totalMessagesInMailbox, int numNewMessages) { - if (mResultCallback != null && isActiveResultCallback(mResultCallback)) { - mResultCallback.updateMailboxCallback(null, account.mId, folder.mId, 100, - totalMessagesInMailbox, numNewMessages); + synchronized (mListeners) { + for (Result l : mListeners) { + l.updateMailboxCallback(null, account.mId, folder.mId, 100, numNewMessages); + } } - mLegacyController.removeListener(this); } @Override public void synchronizeMailboxFailed(EmailContent.Account account, EmailContent.Mailbox folder, Exception e) { - if (mResultCallback != null && isActiveResultCallback(mResultCallback)) { - MessagingException me; - if (e instanceof MessagingException) { - me = (MessagingException) e; - } else { - me = new MessagingException(e.toString()); + synchronized (mListeners) { + for (Result l : mListeners) { + MessagingException me; + if (e instanceof MessagingException) { + me = (MessagingException) e; + } else { + me = new MessagingException(e.toString()); + } + l.updateMailboxCallback(me, account.mId, folder.mId, 0, 0); } - mResultCallback.updateMailboxCallback(me, account.mId, folder.mId, 0, -1, -1); } - mLegacyController.removeListener(this); } + @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); + } + } + } } /** @@ -700,8 +762,7 @@ public class Controller { result = new MessagingException(String.valueOf(statusCode)); break; } - // TODO can we get "number of new messages" back as well? - // TODO remove "total num messages" which can be looked up if needed + // TODO where do we get "number of new messages" as well? // TODO should pass this back instead of looking it up here // TODO smaller projection Mailbox mbx = Mailbox.restoreMailboxWithId(mContext, mailboxId); @@ -710,7 +771,7 @@ public class Controller { long accountId = mbx.mAccountKey; synchronized(mListeners) { for (Result listener : mListeners) { - listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0, 0); + listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0); } } } diff --git a/src/com/android/email/GroupMessagingListener.java b/src/com/android/email/GroupMessagingListener.java index 9c43e9152..af3694913 100644 --- a/src/com/android/email/GroupMessagingListener.java +++ b/src/com/android/email/GroupMessagingListener.java @@ -138,25 +138,20 @@ public class GroupMessagingListener extends MessagingListener { } @Override - synchronized public void checkMailStarted(Context context, EmailContent.Account account) { + synchronized public void checkMailStarted(Context context, long accountId, long tag) { for (MessagingListener l : mListeners) { - l.checkMailStarted(context, account); + l.checkMailStarted(context, accountId, tag); } } @Override - synchronized public void checkMailFinished(Context context, EmailContent.Account account) { + synchronized public void checkMailFinished(Context context, long accountId, long folderId, + long tag) { for (MessagingListener l : mListeners) { - l.checkMailFinished(context, account); + l.checkMailFinished(context, accountId, folderId, tag); } } - @Override - synchronized public void checkMailFailed(Context context, EmailContent.Account account, - String reason) { - // TODO - } - @Override synchronized public void sendPendingMessagesCompleted(EmailContent.Account account) { for (MessagingListener l : mListeners) { diff --git a/src/com/android/email/MessagingController.java b/src/com/android/email/MessagingController.java index 68d268984..06102ce35 100644 --- a/src/com/android/email/MessagingController.java +++ b/src/com/android/email/MessagingController.java @@ -34,6 +34,7 @@ import com.android.email.mail.store.LocalStore.LocalFolder; import com.android.email.mail.store.LocalStore.LocalMessage; import com.android.email.mail.store.LocalStore.PendingCommand; import com.android.email.provider.EmailContent; +import com.android.email.provider.EmailContent.Mailbox; import com.android.email.provider.EmailContent.MailboxColumns; import com.android.email.provider.EmailContent.MessageColumns; import com.android.email.provider.EmailContent.SyncColumns; @@ -219,7 +220,9 @@ public class MessagingController implements Runnable { * @param listener * @throws MessagingException */ - public void listFolders(final EmailContent.Account account, MessagingListener listener) { + public void listFolders(long accountId, MessagingListener listener) { + final EmailContent.Account account = + EmailContent.Account.restoreAccountWithId(mContext, accountId); mListeners.listFoldersStarted(account); put("listFolders", listener, new Runnable() { public void run() { @@ -493,6 +496,7 @@ public class MessagingController implements Runnable { /** * 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 * @param listener @@ -528,49 +532,6 @@ public class MessagingController implements Runnable { mListeners.synchronizeMailboxFailed(account, folder, e); } } - - // TODO move all this to top -/* - public static final int CONTENT_ID_COLUMN = 0; - public static final int CONTENT_DISPLAY_NAME_COLUMN = 1; - public static final int CONTENT_TIMESTAMP_COLUMN = 2; - public static final int CONTENT_SUBJECT_COLUMN = 3; - public static final int CONTENT_PREVIEW_COLUMN = 4; - public static final int CONTENT_FLAG_READ_COLUMN = 5; - public static final int CONTENT_FLAG_LOADED_COLUMN = 6; - public static final int CONTENT_FLAG_FAVORITE_COLUMN = 7; - public static final int CONTENT_FLAG_ATTACHMENT_COLUMN = 8; - public static final int CONTENT_FLAGS_COLUMN = 9; - public static final int CONTENT_TEXT_INFO_COLUMN = 10; - public static final int CONTENT_HTML_INFO_COLUMN = 11; - public static final int CONTENT_BODY_ID_COLUMN = 12; - public static final int CONTENT_SERVER_ID_COLUMN = 13; - public static final int CONTENT_CLIENT_ID_COLUMN = 14; - public static final int CONTENT_MESSAGE_ID_COLUMN = 15; - public static final int CONTENT_THREAD_ID_COLUMN = 16; - public static final int CONTENT_MAILBOX_KEY_COLUMN = 17; - public static final int CONTENT_ACCOUNT_KEY_COLUMN = 18; - public static final int CONTENT_REFERENCE_KEY_COLUMN = 19; - public static final int CONTENT_SENDER_LIST_COLUMN = 20; - public static final int CONTENT_FROM_LIST_COLUMN = 21; - public static final int CONTENT_TO_LIST_COLUMN = 22; - public static final int CONTENT_CC_LIST_COLUMN = 23; - public static final int CONTENT_BCC_LIST_COLUMN = 24; - public static final int CONTENT_REPLY_TO_COLUMN = 25; - public static final int CONTENT_SERVER_VERSION_COLUMN = 26; - public static final String[] CONTENT_PROJECTION = new String[] { - RECORD_ID, MessageColumns.DISPLAY_NAME, MessageColumns.TIMESTAMP, - MessageColumns.SUBJECT, MessageColumns.PREVIEW, MessageColumns.FLAG_READ, - MessageColumns.FLAG_LOADED, MessageColumns.FLAG_FAVORITE, - MessageColumns.FLAG_ATTACHMENT, MessageColumns.FLAGS, MessageColumns.TEXT_INFO, - MessageColumns.HTML_INFO, MessageColumns.BODY_ID, SyncColumns.SERVER_ID, - MessageColumns.CLIENT_ID, MessageColumns.MESSAGE_ID, MessageColumns.THREAD_ID, - MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY, MessageColumns.REFERENCE_KEY, - MessageColumns.SENDER_LIST, MessageColumns.FROM_LIST, MessageColumns.TO_LIST, - MessageColumns.CC_LIST, MessageColumns.BCC_LIST, MessageColumns.REPLY_TO_LIST, - SyncColumns.SERVER_VERSION - }; -*/ /** * Lightweight record for the first pass of message sync, where I'm just seeing if @@ -1135,8 +1096,7 @@ public class MessagingController implements Runnable { return results; } - return new StoreSynchronizer.SyncResults(0, 0); - + return new StoreSynchronizer.SyncResults(remoteMessageCount, newMessages.size()); } private void queuePendingCommand(EmailContent.Account account, PendingCommand command) { @@ -1710,6 +1670,9 @@ public class MessagingController implements Runnable { /** * Attempt to send any messages that are sitting in the Outbox. + * TODO rewrite for database not LocalStore + * TODO this should accept accountId, and probably be reworked in other ways + * * @param account * @param listener */ @@ -1846,51 +1809,36 @@ public class MessagingController implements Runnable { /** * Checks mail for one or multiple accounts. If account is null all accounts - * are checked. + * are checked. This entry point is for use by the mail checking service only, because it + * gives slightly different callbacks (so the service doesn't get confused by callbacks + * triggered by/for the foreground UI. * - * TODO: There is no use case for "check all accounts". Clean up this API to remove - * that case. Callers can supply the appropriate list. - * - * TODO: Better protection against a failure in account n, which should not prevent - * syncing account in accounts n+1 and beyond. + * TODO clean up the execution model which is unnecessarily threaded due to legacy code * * @param context - * @param accounts List of accounts to check, or null to check all accounts + * @param accountId the account to check * @param listener */ - public void checkMail(final Context context, EmailContent.Account[] accounts, - final MessagingListener listener) { - /** - * Note: The somewhat tortured logic here is to guarantee proper ordering of events: - * listeners: checkMailStarted - * account 1: list folders - * account 1: sync messages - * account 2: list folders - * account 2: sync messages - * ... - * account n: list folders - * account n: sync messages - * listeners: checkMailFinished - */ - mListeners.checkMailStarted(context, null); // TODO this needs to pass the actual array - if (accounts == null) { - // TODO eliminate this use case, implement, or ...? -// accounts = Preferences.getPreferences(context).getAccounts(); - } - for (final EmailContent.Account account : accounts) { - listFolders(account, null); + public void checkMail(final long accountId, final long tag, final MessagingListener listener) { + mListeners.checkMailStarted(mContext, accountId, tag); - put("checkMail", listener, new Runnable() { - public void run() { - sendPendingMessagesSynchronous(account); - // TODO find mailbox # for inbox and sync it. -// synchronizeMailboxSynchronous(account, Email.INBOX); - } - }); - } - put("checkMailFinished", listener, new Runnable() { + // This puts the command on the queue (not synchronous) + listFolders(accountId, null); + + // Put this on the queue as well so it follows listFolders + put("emptyTrash", listener, new Runnable() { public void run() { - mListeners.checkMailFinished(context, null); // TODO this needs to pass actual array + EmailContent.Account account = + EmailContent.Account.restoreAccountWithId(mContext, accountId); + sendPendingMessagesSynchronous(account); + // find mailbox # for inbox and sync it. + // TODO we already know this in Controller, can we pass it in? + long inboxId = Mailbox.findMailboxOfType(mContext, accountId, Mailbox.TYPE_INBOX); + EmailContent.Mailbox mailbox = + EmailContent.Mailbox.restoreMailboxWithId(mContext, inboxId); + synchronizeMailboxSynchronous(account, mailbox); + + mListeners.checkMailFinished(mContext, accountId, tag, inboxId); } }); } diff --git a/src/com/android/email/MessagingListener.java b/src/com/android/email/MessagingListener.java index d4fc9941f..8347a5000 100644 --- a/src/com/android/email/MessagingListener.java +++ b/src/com/android/email/MessagingListener.java @@ -71,13 +71,10 @@ public class MessagingListener { String message) { } - public void checkMailStarted(Context context, EmailContent.Account account) { + public void checkMailStarted(Context context, long accountId, long tag) { } - public void checkMailFinished(Context context, EmailContent.Account account) { - } - - public void checkMailFailed(Context context, EmailContent.Account account, String reason) { + public void checkMailFinished(Context context, long accountId, long folderId, long tag) { } public void sendPendingMessagesCompleted(EmailContent.Account account) { diff --git a/src/com/android/email/activity/AccountFolderList.java b/src/com/android/email/activity/AccountFolderList.java index 59a3a13b3..daf488198 100644 --- a/src/com/android/email/activity/AccountFolderList.java +++ b/src/com/android/email/activity/AccountFolderList.java @@ -611,23 +611,34 @@ public class AccountFolderList extends ListActivity } /** - * Callback for async Controller results. This is all a placeholder until we figure out the - * final way to do this. + * Callback for async Controller results. */ private class ControllerResults implements Controller.Result { public void updateMailboxListCallback(MessagingException result, long accountKey, int progress) { - mHandler.progress(false); + if (progress == 0) { + mHandler.progress(true); + } else if (result != null || progress == 100) { + mHandler.progress(false); + } } public void updateMailboxCallback(MessagingException result, long accountKey, - long mailboxKey, int progress, int totalMessagesInMailbox, int numNewMessages) { - mHandler.progress(false); + long mailboxKey, int progress, int numNewMessages) { + if (progress == 0) { + mHandler.progress(true); + } else if (result != null || progress == 100) { + mHandler.progress(false); + } } public void loadAttachmentCallback(MessagingException result, long messageId, long attachmentId, int progress) { } + + public void serviceCheckMailCallback(MessagingException result, long accountId, + long mailboxId, int progress, long tag) { + } } /* package */ static class AccountsAdapter extends CursorAdapter { diff --git a/src/com/android/email/activity/FolderMessageList.java b/src/com/android/email/activity/FolderMessageList.java index ad7b55c38..e9fe851b1 100644 --- a/src/com/android/email/activity/FolderMessageList.java +++ b/src/com/android/email/activity/FolderMessageList.java @@ -755,14 +755,14 @@ public class FolderMessageList extends ExpandableListActivity { private void doRefreshOpenMailbox() { if (this.mExpandedGroup != -1) { Cursor mailboxCursor = mNewAdapter.getGroup(mExpandedGroup); - EmailContent.Mailbox mailbox = - EmailContent.getContent(mailboxCursor, EmailContent.Mailbox.class); - - if (mailbox != null) { - mHandler.progress(true); - Controller.getInstance(getApplication()). - updateMailbox(mAccount.mId, mailbox, mControllerCallback); - } +// EmailContent.Mailbox mailbox = +// EmailContent.getContent(mailboxCursor, EmailContent.Mailbox.class); +// +// if (mailbox != null) { +// mHandler.progress(true); +// Controller.getInstance(getApplication()). +// updateMailbox(mAccount.mId, mailbox, mControllerCallback); +// } } } @@ -778,13 +778,17 @@ public class FolderMessageList extends ExpandableListActivity { } public void updateMailboxCallback(MessagingException result, long accountKey, - long mailboxKey, int progress, int totalMessagesInMailbox, int numNewMessages) { + long mailboxKey, int progress, int numNewMessages) { mHandler.progress(false); } public void loadAttachmentCallback(MessagingException result, long messageId, long attachmentId, int progress) { } + + public void serviceCheckMailCallback(MessagingException result, long accountId, + long mailboxId, int progress, long tag) { + } } @Deprecated diff --git a/src/com/android/email/activity/MailboxList.java b/src/com/android/email/activity/MailboxList.java index 091adf0ca..eb3588783 100644 --- a/src/com/android/email/activity/MailboxList.java +++ b/src/com/android/email/activity/MailboxList.java @@ -275,8 +275,7 @@ public class MailboxList extends ListActivity implements OnItemClickListener, On Controller controller = Controller.getInstance(getApplication()); mHandler.progress(true); if (mailboxId >= 0) { - Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mailboxId); - controller.updateMailbox(mAccountId, mailbox, mControllerCallback); + controller.updateMailbox(mAccountId, mailboxId, mControllerCallback); } else { controller.updateMailboxList(mAccountId, mControllerCallback); } @@ -376,8 +375,7 @@ public class MailboxList extends ListActivity implements OnItemClickListener, On if (accountKey == mAccountId) { if (progress == 0) { mHandler.progress(true); - } - else if (result != null || progress == 100) { + } else if (result != null || progress == 100) { mHandler.progress(false); } } @@ -385,12 +383,11 @@ public class MailboxList extends ListActivity implements OnItemClickListener, On // TODO report errors into UI public void updateMailboxCallback(MessagingException result, long accountKey, - long mailboxKey, int progress, int totalMessagesInMailbox, int numNewMessages) { + long mailboxKey, int progress, int numNewMessages) { if (accountKey == mAccountId) { if (progress == 0) { mHandler.progress(true); - } - else if (result != null || progress == 100) { + } else if (result != null || progress == 100) { mHandler.progress(false); } } @@ -399,6 +396,10 @@ public class MailboxList extends ListActivity implements OnItemClickListener, On public void loadAttachmentCallback(MessagingException result, long messageId, long attachmentId, int progress) { } + + public void serviceCheckMailCallback(MessagingException result, long accountId, + long mailboxId, int progress, long tag) { + } } /** diff --git a/src/com/android/email/activity/MessageCompose.java b/src/com/android/email/activity/MessageCompose.java index 98bf16276..60acc87ad 100644 --- a/src/com/android/email/activity/MessageCompose.java +++ b/src/com/android/email/activity/MessageCompose.java @@ -20,14 +20,10 @@ import com.android.email.Controller; import com.android.email.Email; import com.android.email.EmailAddressAdapter; import com.android.email.EmailAddressValidator; -import com.android.email.MessagingController; import com.android.email.R; import com.android.email.Utility; import com.android.email.mail.Address; import com.android.email.mail.MessagingException; -import com.android.email.mail.Multipart; -import com.android.email.mail.Part; -import com.android.email.mail.Message.RecipientType; import com.android.email.mail.internet.EmailHtmlUtil; import com.android.email.mail.internet.MimeUtility; import com.android.email.provider.EmailContent; @@ -56,7 +52,6 @@ import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextWatcher; import android.text.util.Rfc822Tokenizer; -import android.util.Config; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -72,7 +67,6 @@ import android.widget.LinearLayout; import android.widget.MultiAutoCompleteTextView; import android.widget.TextView; import android.widget.Toast; -import android.widget.AutoCompleteTextView.Validator; import java.io.Serializable; import java.io.UnsupportedEncodingException; @@ -1232,12 +1226,16 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus } public void updateMailboxCallback(MessagingException result, long accountId, - long mailboxId, int progress, int totalMessagesInMailbox, int numNewMessages) { + long mailboxId, int progress, int numNewMessages) { } public void loadAttachmentCallback(MessagingException result, long messageId, long attachmentId, int progress) { } + + public void serviceCheckMailCallback(MessagingException result, long accountId, + long mailboxId, int progress, long tag) { + } } // class Listener extends MessagingListener { diff --git a/src/com/android/email/activity/MessageList.java b/src/com/android/email/activity/MessageList.java index 86f79c8bf..dfcbae5b4 100644 --- a/src/com/android/email/activity/MessageList.java +++ b/src/com/android/email/activity/MessageList.java @@ -28,8 +28,10 @@ import com.android.email.provider.EmailContent.Mailbox; import com.android.email.provider.EmailContent.MailboxColumns; import com.android.email.provider.EmailContent.Message; import com.android.email.provider.EmailContent.MessageColumns; +import com.android.email.service.MailService; import android.app.ListActivity; +import android.app.NotificationManager; import android.content.ContentUris; import android.content.Context; import android.content.Intent; @@ -180,13 +182,15 @@ public class MessageList extends ListActivity implements OnItemClickListener, On * notifications. * * @param context The caller's context (for generating an intent) - * @param accountId The account to open - * @param mailboxType the type of mailbox to open (e.g. @see EmailContent.Mailbox.TYPE_INBOX) + * @param accountId The account to open, or -1 + * @param mailboxId the ID of the mailbox to open, or -1 + * @param mailboxType the type of mailbox to open (e.g. @see Mailbox.TYPE_INBOX) or -1 */ public static Intent actionHandleAccountIntent(Context context, long accountId, - int mailboxType) { + long mailboxId, int mailboxType) { Intent intent = new Intent(context, MessageList.class); intent.putExtra(EXTRA_ACCOUNT_ID, accountId); + intent.putExtra(EXTRA_MAILBOX_ID, mailboxId); intent.putExtra(EXTRA_MAILBOX_TYPE, mailboxType); return intent; } @@ -201,7 +205,7 @@ public class MessageList extends ListActivity implements OnItemClickListener, On */ public static Intent actionHandleAccountUriIntent(Context context, long accountId, int mailboxType) { - Intent i = actionHandleAccountIntent(context, accountId, mailboxType); + Intent i = actionHandleAccountIntent(context, accountId, -1, mailboxType); i.removeExtra(EXTRA_ACCOUNT_ID); Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); i.setData(uri); @@ -241,7 +245,7 @@ public class MessageList extends ListActivity implements OnItemClickListener, On // Specific mailbox ID was provided - go directly to it mSetTitleTask = new SetTitleTask(mMailboxId); mSetTitleTask.execute(); - mLoadMessagesTask = new LoadMessagesTask(mMailboxId); + mLoadMessagesTask = new LoadMessagesTask(mMailboxId, -1); mLoadMessagesTask.execute(); } else { long accountId = -1; @@ -278,8 +282,11 @@ public class MessageList extends ListActivity implements OnItemClickListener, On public void onResume() { super.onResume(); Controller.getInstance(getApplication()).addResultCallback(mControllerCallback); - - // TODO: may need to clear notifications here + + // clear notifications here + NotificationManager notificationManager = (NotificationManager) + getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(MailService.NEW_MESSAGE_NOTIFICATION_ID); } @Override @@ -410,12 +417,11 @@ public class MessageList extends ListActivity implements OnItemClickListener, On private void onRefresh() { // TODO: This needs to loop through all open mailboxes (there might be more than one) - // TODO: Should not be reading from DB in UI thread + // TODO: Should not be reading from DB in UI thread - need a cleaner way to get accountId if (mMailboxId >= 0) { - EmailContent.Mailbox mailbox = - EmailContent.Mailbox.restoreMailboxWithId(this, mMailboxId); + Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mMailboxId); Controller.getInstance(getApplication()).updateMailbox( - mailbox.mAccountKey, mailbox, mControllerCallback); + mailbox.mAccountKey, mMailboxId, mControllerCallback); } } @@ -651,7 +657,7 @@ public class MessageList extends ListActivity implements OnItemClickListener, On mMailboxId = mailboxId; mSetTitleTask = new SetTitleTask(mMailboxId); mSetTitleTask.execute(); - mLoadMessagesTask = new LoadMessagesTask(mMailboxId); + mLoadMessagesTask = new LoadMessagesTask(mMailboxId, mAccountId); mLoadMessagesTask.execute(); } } @@ -669,12 +675,14 @@ public class MessageList extends ListActivity implements OnItemClickListener, On private class LoadMessagesTask extends AsyncTask { private long mMailboxKey; + private long mAccountKey; /** * Special constructor to cache some local info */ - public LoadMessagesTask(long mailboxKey) { + public LoadMessagesTask(long mailboxKey, long accountKey) { mMailboxKey = mailboxKey; + mAccountKey = accountKey; } @Override @@ -743,6 +751,13 @@ public class MessageList extends ListActivity implements OnItemClickListener, On if (cursor != null && cursor.getCount() == 0) { onRefresh(); } + + // Reset the "new messages" count in the service, since we're seeing them now + if (mMailboxKey == QUERY_ALL_INBOXES) { + MailService.resetNewMessageCount(-1); + } else if (mMailboxKey >= 0 && mAccountKey != -1) { + MailService.resetNewMessageCount(mAccountKey); + } } } @@ -866,8 +881,7 @@ public class MessageList extends ListActivity implements OnItemClickListener, On int progress) { if (progress == 0) { mHandler.progress(true); - } - else if (result != null || progress == 100) { + } else if (result != null || progress == 100) { mHandler.progress(false); if (mWaitForMailboxType != -1) { if (result == null) { @@ -880,11 +894,10 @@ public class MessageList extends ListActivity implements OnItemClickListener, On // TODO report errors into UI // TODO check accountKey and only react to relevant notifications public void updateMailboxCallback(MessagingException result, long accountKey, - long mailboxKey, int progress, int totalMessagesInMailbox, int numNewMessages) { + long mailboxKey, int progress, int numNewMessages) { if (progress == 0) { mHandler.progress(true); - } - else if (result != null || progress == 100) { + } else if (result != null || progress == 100) { mHandler.progress(false); } } @@ -892,6 +905,10 @@ public class MessageList extends ListActivity implements OnItemClickListener, On public void loadAttachmentCallback(MessagingException result, long messageId, long attachmentId, int progress) { } + + public void serviceCheckMailCallback(MessagingException result, long accountId, + long mailboxId, int progress, long tag) { + } } /** diff --git a/src/com/android/email/activity/MessageView.java b/src/com/android/email/activity/MessageView.java index 0ee2d6a5e..ccda54a4e 100644 --- a/src/com/android/email/activity/MessageView.java +++ b/src/com/android/email/activity/MessageView.java @@ -25,10 +25,8 @@ import com.android.email.Utility; import com.android.email.mail.Address; import com.android.email.mail.MessagingException; import com.android.email.mail.Part; -import com.android.email.mail.Message.RecipientType; import com.android.email.mail.internet.EmailHtmlUtil; import com.android.email.mail.internet.MimeUtility; -import com.android.email.mail.store.LocalStore.LocalMessage; import com.android.email.provider.AttachmentProvider; import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.Attachment; @@ -59,7 +57,6 @@ import android.provider.Contacts; import android.provider.Contacts.Intents; import android.provider.Contacts.People; import android.text.util.Regex; -import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -1227,12 +1224,16 @@ public class MessageView extends Activity } public void updateMailboxCallback(MessagingException result, long accountId, - long mailboxId, int progress, int totalMessagesInMailbox, int numNewMessages) { + long mailboxId, int progress, int numNewMessages) { } public void updateMailboxListCallback(MessagingException result, long accountId, int progress) { } + + public void serviceCheckMailCallback(MessagingException result, long accountId, + long mailboxId, int progress, long tag) { + } } /** diff --git a/src/com/android/email/mail/Store.java b/src/com/android/email/mail/Store.java index 4956a9836..c5b2bf390 100644 --- a/src/com/android/email/mail/Store.java +++ b/src/com/android/email/mail/Store.java @@ -242,27 +242,6 @@ public abstract class Store { public abstract void checkSettings() throws MessagingException; - /** - * Enable or disable push mode delivery for a given Store. - * - *

For protocols that do not support push mode, be sure that push="true" is not - * set by the stores.xml definition file(s). This function need not be implemented. - * - *

For protocols that do support push mode, this will be called at startup (boot) time - * so that the Store can launch its own underlying connection service. It will also be called - * any time the user changes the settings for the account (because the user may switch back - * to polling mode (or disable checking completely). - * - *

This API will be called repeatedly, even after push mode has already been started or - * stopped. Stores that support push mode should return quickly if the configuration has not - * changed. - * - * @param enablePushMode start or stop push mode delivery - */ - public void enablePushModeDelivery(boolean enablePushMode) { - // does nothing for non-push protocols - } - /** * Delete Store and its corresponding resources. * @throws MessagingException diff --git a/src/com/android/email/mail/exchange/ExchangeStoreExample.java b/src/com/android/email/mail/exchange/ExchangeStoreExample.java index 402fc076a..91ce37e62 100644 --- a/src/com/android/email/mail/exchange/ExchangeStoreExample.java +++ b/src/com/android/email/mail/exchange/ExchangeStoreExample.java @@ -49,8 +49,6 @@ public class ExchangeStoreExample extends Store { private final ExchangeTransportExample mTransport; private final HashMap mFolders = new HashMap(); - private boolean mPushModeRunning = false; - /** * Factory method. */ @@ -120,30 +118,7 @@ public class ExchangeStoreExample extends Store { getFolder(ExchangeTransportExample.FOLDER_INBOX), }; } - - /** - * For a store that supports push mode, this is the API that enables it or disables it. - * The store should use this API to start or stop its persistent connection service or thread. - * - *

Note, may be called multiple times, even after push mode has been started or stopped. - * - * @param enablePushMode start or stop push mode delivery - */ - @Override - public void enablePushModeDelivery(boolean enablePushMode) { - if (Config.LOGD && Email.DEBUG) { - if (enablePushMode && !mPushModeRunning) { - Log.d(Email.LOG_TAG, "start push mode"); - } else if (!enablePushMode && mPushModeRunning) { - Log.d(Email.LOG_TAG, "stop push mode"); - } else { - Log.d(Email.LOG_TAG, enablePushMode ? - "push mode already started" : "push mode already stopped"); - } - } - mPushModeRunning = enablePushMode; - } - + /** * Get class of SettingActivity for this Store class. * @return Activity class that has class method actionEditIncomingSettings(). diff --git a/src/com/android/email/mail/store/ExchangeStore.java b/src/com/android/email/mail/store/ExchangeStore.java index 4a89d2cfe..15e0cd1cb 100644 --- a/src/com/android/email/mail/store/ExchangeStore.java +++ b/src/com/android/email/mail/store/ExchangeStore.java @@ -41,7 +41,6 @@ import android.content.Context; import android.os.Bundle; import android.os.RemoteException; import android.text.TextUtils; -import android.util.Config; import android.util.Log; import java.io.IOException; @@ -64,8 +63,6 @@ public class ExchangeStore extends Store { private final ExchangeTransport mTransport; private final HashMap mFolders = new HashMap(); - private boolean mPushModeRunning = false; - /** * Factory method. */ @@ -164,29 +161,6 @@ public class ExchangeStore extends Store { }; } - /** - * For a store that supports push mode, this is the API that enables it or disables it. - * The store should use this API to start or stop its persistent connection service or thread. - * - *

Note, may be called multiple times, even after push mode has been started or stopped. - * - * @param enablePushMode start or stop push mode delivery - */ - @Override - public void enablePushModeDelivery(boolean enablePushMode) { - if (Config.LOGD && Email.DEBUG) { - if (enablePushMode && !mPushModeRunning) { - Log.d(Email.LOG_TAG, "start push mode"); - } else if (!enablePushMode && mPushModeRunning) { - Log.d(Email.LOG_TAG, "stop push mode"); - } else { - Log.d(Email.LOG_TAG, enablePushMode ? - "push mode already started" : "push mode already stopped"); - } - } - mPushModeRunning = enablePushMode; - } - /** * Get class of SettingActivity for this Store class. * @return Activity class that has class method actionEditIncomingSettings() diff --git a/src/com/android/email/service/MailService.java b/src/com/android/email/service/MailService.java index 48e48b74f..01537fd82 100644 --- a/src/com/android/email/service/MailService.java +++ b/src/com/android/email/service/MailService.java @@ -16,52 +16,60 @@ package com.android.email.service; -import com.android.email.Account; +import com.android.email.Controller; import com.android.email.Email; -import com.android.email.MessagingController; -import com.android.email.MessagingListener; import com.android.email.R; -import com.android.email.activity.AccountFolderList; import com.android.email.activity.MessageList; import com.android.email.mail.MessagingException; -import com.android.email.mail.Store; -import com.android.email.mail.store.LocalStore; -import com.android.email.provider.EmailContent; +import com.android.email.provider.EmailContent.Account; +import com.android.email.provider.EmailContent.Mailbox; import android.app.AlarmManager; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; +import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.IBinder; import android.os.SystemClock; -import android.text.TextUtils; import android.util.Config; import android.util.Log; -import java.util.ArrayList; import java.util.HashMap; /** + * Background service for refreshing non-push email accounts. */ public class MailService extends Service { + /** DO NOT CHECK IN "TRUE" */ + private static final boolean DEBUG_FORCE_QUICK_REFRESH = false; // force 1-minute refresh + + public static int NEW_MESSAGE_NOTIFICATION_ID = 1; + 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 EXTRA_CHECK_ACCOUNT = "com.android.email.intent.extra.ACCOUNT"; - private Listener mListener = new Listener(); + private static final String EXTRA_CHECK_ACCOUNT = "com.android.email.intent.extra.ACCOUNT"; + private static final String EXTRA_ACCOUNT_INFO = "com.android.email.intent.extra.ACCOUNT_INFO"; + + private Controller.Result mControllerCallback = new ControllerResults(); private int mStartId; + /** + * Access must be synchronized, because there are accesses from the Controller callback + */ + private static HashMap mSyncReports = + new HashMap(); + public static void actionReschedule(Context context) { Intent i = new Intent(); i.setClass(context, MailService.class); @@ -75,6 +83,23 @@ public class MailService extends Service { i.setAction(MailService.ACTION_CANCEL); context.startService(i); } + + /** + * Reset new message counts for one or all accounts + * + * TODO what about EAS service new message counts, where are they reset? + * + * @param accountId account to clear, or -1 for all accounts + */ + public static void resetNewMessageCount(long accountId) { + synchronized (mSyncReports) { + for (AccountSyncReport report : mSyncReports.values()) { + if (accountId == -1 || accountId == report.accountId) { + report.numNewMessages = 0; + } + } + } + } /** * Entry point for asynchronous message services (e.g. push mode) to post notifications of new @@ -82,66 +107,44 @@ public class MailService extends Service { * which will attempt to load the new messages. So the Store should expect to be opened and * fetched from shortly after making this call. * - * @param storeUri the Uri of the store that is reporting new messages + * @param accountId the id of the account that is reporting new messages */ - public static void actionNotifyNewMessages(Context context, String storeUri) { + public static void actionNotifyNewMessages(Context context, long accountId) { Intent i = new Intent(ACTION_CHECK_MAIL); i.setClass(context, MailService.class); - i.putExtra(EXTRA_CHECK_ACCOUNT, storeUri); + i.putExtra(EXTRA_CHECK_ACCOUNT, accountId); context.startService(i); } @Override public void onStart(Intent intent, int startId) { super.onStart(intent, startId); + + // TODO this needs to be passed through the controller and back to us this.mStartId = startId; - MessagingController controller = MessagingController.getInstance(getApplication()); - controller.addListener(mListener); + Controller controller = Controller.getInstance(getApplication()); + controller.addResultCallback(mControllerCallback); + if (ACTION_CHECK_MAIL.equals(intent.getAction())) { if (Config.LOGD && Email.DEBUG) { Log.d(Email.LOG_TAG, "*** MailService: checking mail"); } - // Only check mail for accounts that have enabled automatic checking. There is still - // a bug here in that we check every enabled account, on every refresh - irrespective - // of that account's refresh frequency - but this fixes the worst case of checking - // accounts that should not have been checked at all. - // Also note: Due to the organization of this service, you must gather the accounts - // and make a single call to controller.checkMail(). + // 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); - // TODO: Notification for single push account will fire up checks on all other - // accounts. This needs to be cleaned up for better efficiency. - String specificStoreUri = intent.getStringExtra(EXTRA_CHECK_ACCOUNT); - - ArrayList accountsToCheck = new ArrayList(); - - Cursor c = null; - try { - c = this.getContentResolver().query( - EmailContent.Account.CONTENT_URI, - EmailContent.Account.CONTENT_PROJECTION, - null, null, null); - while (c.moveToNext()) { - EmailContent.Account account = EmailContent.getContent(c, - EmailContent.Account.class); - int interval = account.getSyncInterval(); - String storeUri = account.getStoreUri(this); - if (interval > 0 || (storeUri != null && storeUri.equals(specificStoreUri))) { - accountsToCheck.add(account); - } - - // For each account, switch pushmail on or off - enablePushMail(account, interval == EmailContent.Account.CHECK_INTERVAL_PUSH); - } - } finally { - if (c != null) { - c.close(); - } + // Sync a specific account if given + long checkAccountId = intent.getLongExtra(EXTRA_CHECK_ACCOUNT, -1); + if (checkAccountId != -1) { + // launch an account sync in the controller + syncOneAccount(controller, checkAccountId, startId); + } else { + // Find next account to sync, and reschedule + AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE); + reschedule(alarmManager); + stopSelf(startId); } - - EmailContent.Account[] accounts = accountsToCheck.toArray( - new EmailContent.Account[accountsToCheck.size()]); - controller.checkMail(this, accounts, mListener); } else if (ACTION_CANCEL.equals(intent.getAction())) { if (Config.LOGD && Email.DEBUG) { @@ -154,186 +157,366 @@ public class MailService extends Service { if (Config.LOGD && Email.DEBUG) { Log.d(Email.LOG_TAG, "*** MailService: reschedule"); } - reschedule(); + AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE); + reschedule(alarmManager); stopSelf(startId); } } - @Override - public void onDestroy() { - super.onDestroy(); - MessagingController.getInstance(getApplication()).removeListener(mListener); - } - - private void cancel() { - AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE); - Intent i = new Intent(); - i.setClassName("com.android.email", "com.android.email.service.MailService"); - i.setAction(ACTION_CHECK_MAIL); - PendingIntent pi = PendingIntent.getService(this, 0, i, 0); - alarmMgr.cancel(pi); - } - - private void reschedule() { - AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE); - Intent i = new Intent(); - i.setClassName("com.android.email", "com.android.email.service.MailService"); - i.setAction(ACTION_CHECK_MAIL); - PendingIntent pi = PendingIntent.getService(this, 0, i, 0); - - int shortestInterval = -1; - Cursor c = null; - try { - c = this.getContentResolver().query( - EmailContent.Account.CONTENT_URI, - EmailContent.Account.CONTENT_PROJECTION, - null, null, null); - while (c.moveToNext()) { - EmailContent.Account account = EmailContent.getContent(c, - EmailContent.Account.class); - int interval = account.getSyncInterval(); - if (interval > 0 && (interval < shortestInterval || shortestInterval == -1)) { - shortestInterval = interval; - } - enablePushMail(account, interval == Account.CHECK_INTERVAL_PUSH); - } - } finally { - if (c != null) { - c.close(); - } - } - - if (shortestInterval == -1) { - alarmMgr.cancel(pi); - } - else { - alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() - + (shortestInterval * (60 * 1000)), pi); - } - } - public IBinder onBind(Intent intent) { return null; } - class Listener extends MessagingListener { - HashMap accountsWithNewMail = - new HashMap(); + @Override + public void onDestroy() { + super.onDestroy(); + Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback); + } - // TODO this should be redone because account is usually null, not very interesting. - // I think it would make more sense to pass Account[] here in case anyone uses it - // In any case, it should be noticed that this is called once per cycle - @Override - public void checkMailStarted(Context context, EmailContent.Account account) { - accountsWithNewMail.clear(); - } + private void cancel() { + AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE); + PendingIntent pi = createAlarmIntent(-1, null); + alarmMgr.cancel(pi); + } - // Called once per checked account - @Override - public void checkMailFailed(Context context, EmailContent.Account account, String reason) { - if (Config.LOGD && Email.DEBUG) { - Log.d(Email.LOG_TAG, "*** MailService: checkMailFailed: " + reason); - } - reschedule(); - stopSelf(mStartId); - } + /** + * 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. + */ + /* package */ void reschedule(AlarmManager alarmMgr) { + // restore the reports if lost + setupSyncReports(-1); + synchronized (mSyncReports) { + int numAccounts = mSyncReports.size(); + long[] accountInfo = new long[numAccounts * 2]; // pairs of { accountId, lastSync } + int accountInfoIndex = 0; - // Called once per checked account - @Override - public void synchronizeMailboxFinished(EmailContent.Account account, - EmailContent.Mailbox folder, int totalMessagesInMailbox, int numNewMessages) { - if (Config.LOGD && Email.DEBUG) { - Log.d(Email.LOG_TAG, "*** MailService: synchronizeMailboxFinished: total=" + - totalMessagesInMailbox + " new=" + numNewMessages); - } - if (numNewMessages > 0 && - ((account.getFlags() & EmailContent.Account.FLAGS_NOTIFY_NEW_MAIL) != 0)) { - accountsWithNewMail.put(account, numNewMessages); - } - } + long nextCheckTime = Long.MAX_VALUE; + AccountSyncReport nextAccount = null; + long timeNow = SystemClock.elapsedRealtime(); - // TODO this should be redone because account is usually null, not very interesting. - // I think it would make more sense to pass Account[] here in case anyone uses it - // In any case, it should be noticed that this is called once per cycle - @Override - public void checkMailFinished(Context context, EmailContent.Account account) { - if (Config.LOGD && Email.DEBUG) { - Log.d(Email.LOG_TAG, "*** MailService: checkMailFinished"); - } - NotificationManager notifMgr = (NotificationManager)context - .getSystemService(Context.NOTIFICATION_SERVICE); - - if (accountsWithNewMail.size() > 0) { - Notification notif = new Notification(R.drawable.stat_notify_email_generic, - getString(R.string.notification_new_title), System.currentTimeMillis()); - boolean vibrate = false; - String ringtone = null; - if (accountsWithNewMail.size() > 1) { - for (EmailContent.Account account1 : accountsWithNewMail.keySet()) { - if ((account1.getFlags() & EmailContent.Account.FLAGS_VIBRATE) != 0) { - vibrate = true; - } - ringtone = account1.getRingtone(); - } - Intent i = new Intent(context, AccountFolderList.class); - PendingIntent pi = PendingIntent.getActivity(context, 0, i, 0); - notif.setLatestEventInfo(context, getString(R.string.notification_new_title), - getResources(). - getQuantityString(R.plurals.notification_new_multi_account_fmt, - accountsWithNewMail.size(), - accountsWithNewMail.size()), pi); - } else { - EmailContent.Account account1 = accountsWithNewMail.keySet().iterator().next(); - int totalNewMails = accountsWithNewMail.get(account1); - Intent i = MessageList.actionHandleAccountIntent(context, - account1.mId, EmailContent.Mailbox.TYPE_INBOX); - PendingIntent pi = PendingIntent.getActivity(context, 0, i, 0); - notif.setLatestEventInfo(context, getString(R.string.notification_new_title), - getResources(). - getQuantityString(R.plurals.notification_new_one_account_fmt, - totalNewMails, totalNewMails, - account1.getDisplayName()), pi); - vibrate = ((account1.getFlags() & EmailContent.Account.FLAGS_VIBRATE) != 0); - ringtone = account1.getRingtone(); + for (AccountSyncReport report : mSyncReports.values()) { + if (report.syncInterval <= 0) { // no timed checks - skip + continue; } - notif.defaults = Notification.DEFAULT_LIGHTS; - notif.sound = TextUtils.isEmpty(ringtone) ? null : Uri.parse(ringtone); - if (vibrate) { - notif.defaults |= Notification.DEFAULT_VIBRATE; + // select next account to sync + if ((report.prevSyncTime == 0) // never checked + || (report.nextSyncTime < timeNow)) { // overdue + nextCheckTime = 0; + nextAccount = report; + } else if (report.nextSyncTime < nextCheckTime) { // next to be checked + nextCheckTime = report.nextSyncTime; + nextAccount = report; } - notifMgr.notify(1, notif); + // 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; } - reschedule(); - stopSelf(mStartId); + // set/clear alarm as needed + long idToCheck = (nextAccount == null) ? -1 : nextAccount.accountId; + PendingIntent pi = createAlarmIntent(idToCheck, accountInfo); + + if (nextAccount == null) { + alarmMgr.cancel(pi); + Log.d(Email.LOG_TAG, "alarm cancel - no account to check"); + } else { + alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi); + Log.d(Email.LOG_TAG, "alarm set at " + nextCheckTime + " for " + nextAccount); + } } } /** - * For any account that wants push mail, get its Store and start the pushmail service. - * This function makes no attempt to optimize, so accounts may have push enabled (or disabled) - * repeatedly, and should handle this appropriately. - * - * @param account the account that needs push delivery enabled + * 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 void enablePushMail(EmailContent.Account account, boolean enable) { - try { - String localUri = account.getLocalStoreUri(this); - String storeUri = account.getStoreUri(this); - if (localUri != null && storeUri != null) { - LocalStore localStore = (LocalStore) Store.getInstance( - localUri, this.getBaseContext(), null); - Store store = Store.getInstance(storeUri, this.getBaseContext(), - localStore.getPersistentCallbacks()); - if (store != null) { - store.enablePushModeDelivery(enable); + /* package */ PendingIntent createAlarmIntent(long checkId, long[] accountInfo) { + Intent i = new Intent(); + i.setClassName("com.android.email", "com.android.email.service.MailService"); + i.setAction(ACTION_CHECK_MAIL); + i.putExtra(EXTRA_CHECK_ACCOUNT, checkId); + i.putExtra(EXTRA_ACCOUNT_INFO, accountInfo); + PendingIntent pi = PendingIntent.getService(this, 0, i, 0); + return pi; + } + + /** + * Start a controller sync for a specific account + */ + private void syncOneAccount(Controller controller, long checkAccountId, int startId) { + long inboxId = Mailbox.findMailboxOfType(this, checkAccountId, Mailbox.TYPE_INBOX); + if (inboxId == Mailbox.NO_MAILBOX) { + // no inbox?? sync mailboxes + } else { + controller.serviceCheckMail(checkAccountId, inboxId, startId, mControllerCallback); + } + } + + /** + * Note: Times are relative to SystemClock.elapsedRealtime() + */ + private static class AccountSyncReport { + long accountId; + long prevSyncTime; // 0 == unknown + long nextSyncTime; // 0 == ASAP -1 == don't sync + int numNewMessages; + + int syncInterval; + boolean notify; + boolean vibrate; + Uri ringtoneUri; + + String displayName; // temporary, for debug logging + + public String toString() { + return displayName + ": prevSync=" + prevSyncTime + " nextSync=" + nextSyncTime + + " numNew=" + numNewMessages; + } + } + + /** + * 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) + */ + /* package */ void setupSyncReports(long accountId) { + synchronized (mSyncReports) { + if (accountId == -1) { + // -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; } } - } catch (MessagingException me) { - if (Config.LOGD && Email.DEBUG) { - Log.d(Email.LOG_TAG, "Failed to enable push mail for account" + - account.getSenderName() + " with exception " + me.toString()); + + // setup to add a single account or all accounts + Uri uri; + if (accountId == -1) { + uri = Account.CONTENT_URI; + } else { + uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); + } + + // TODO use a narrower projection here + Cursor c = getContentResolver().query(uri, Account.CONTENT_PROJECTION, + null, null, null); + try { + while (c.moveToNext()) { + AccountSyncReport report = new AccountSyncReport(); + int syncInterval = c.getInt(Account.CONTENT_SYNC_INTERVAL_COLUMN); + int flags = c.getInt(Account.CONTENT_FLAGS_COLUMN); + String ringtoneString = c.getString(Account.CONTENT_RINGTONE_URI_COLUMN); + + // For debugging only + if (DEBUG_FORCE_QUICK_REFRESH && syncInterval >= 0) { + syncInterval = 1; + } + + report.accountId = c.getLong(Account.CONTENT_ID_COLUMN); + report.prevSyncTime = 0; + report.nextSyncTime = (syncInterval > 0) ? 0 : -1; // 0 == ASAP -1 == no sync + report.numNewMessages = 0; + + report.syncInterval = syncInterval; + report.notify = (flags & Account.FLAGS_NOTIFY_NEW_MAIL) != 0; + report.vibrate = (flags & Account.FLAGS_VIBRATE) != 0; + report.ringtoneUri = (ringtoneString == null) ? null + : Uri.parse(ringtoneString); + + report.displayName = c.getString(Account.CONTENT_DISPLAY_NAME_COLUMN); + + // 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 udpated + * @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) + */ + /* package */ 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(Email.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(); + if (report.syncInterval > 0) { + report.nextSyncTime = report.prevSyncTime + (report.syncInterval * 1000 * 60); + } + if (newCount != -1) { + report.numNewMessages = newCount; + } + Log.d(Email.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 + */ + /* package */ void restoreSyncReports(Intent restoreIntent) { + // restore the reports if lost + setupSyncReports(-1); + synchronized (mSyncReports) { + long[] accountInfo = restoreIntent.getLongArrayExtra(EXTRA_ACCOUNT_INFO); + if (accountInfo == null) { + Log.d(Email.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; + Log.d(Email.LOG_TAG, "restore prev sync for account" + report); + } + } + } + } + } + + class ControllerResults implements Controller.Result { + + public void loadAttachmentCallback(MessagingException result, long messageId, + long attachmentId, int progress) { + } + + public void updateMailboxCallback(MessagingException result, long accountId, + long mailboxId, int progress, int numNewMessages) { + if (result == null) { + updateAccountReport(accountId, numNewMessages); + if (numNewMessages > 0) { + notifyNewMessages(accountId); + } + } else { + updateAccountReport(accountId, -1); + } + } + + public void updateMailboxListCallback(MessagingException result, long accountId, + int progress) { + } + + public void serviceCheckMailCallback(MessagingException result, long accountId, + long mailboxId, int progress, long tag) { + if (progress == 100) { + AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE); + reschedule(alarmManager); + int serviceId = MailService.this.mStartId; + if (tag != 0) { + serviceId = (int) tag; + } + stopSelf(serviceId); + } + } + } + + /** + * Prepare notifications for a given new account having received mail + * The notification is organized around the account that has the new mail (e.g. selecting + * the alert preferences) but the notification will include a summary if other + * accounts also have new mail. + */ + private void notifyNewMessages(long accountId) { + boolean notify = false; + boolean vibrate = false; + Uri ringtone = null; + int accountsWithNewMessages = 0; + int numNewMessages = 0; + String reportName = null; + synchronized (mSyncReports) { + for (AccountSyncReport report : mSyncReports.values()) { + if (report.numNewMessages == 0) { + continue; + } + numNewMessages += report.numNewMessages; + accountsWithNewMessages += 1; + if (report.accountId == accountId) { + notify = report.notify; + vibrate = report.vibrate; + ringtone = report.ringtoneUri; + reportName = report.displayName; + } + } + } + if (!notify) { + return; + } + + // set up to post a notification + Intent intent; + String reportString; + + if (accountsWithNewMessages == 1) { + // Prepare a report for a single account + // "12 unread (gmail)" + reportString = getResources().getQuantityString( + R.plurals.notification_new_one_account_fmt, numNewMessages, + numNewMessages, reportName); + intent = MessageList.actionHandleAccountIntent(this, + accountId, -1, Mailbox.TYPE_INBOX); + } else { + // Prepare a report for multiple accounts + // "4 accounts" + reportString = getResources().getQuantityString( + R.plurals.notification_new_multi_account_fmt, accountsWithNewMessages, + accountsWithNewMessages); + intent = MessageList.actionHandleAccountIntent(this, + -1, MessageList.QUERY_ALL_INBOXES, -1); + } + + // prepare appropriate pending intent, set up notification, and send + PendingIntent pending = PendingIntent.getActivity(this, 0, intent, 0); + + Notification notification = new Notification( + R.drawable.stat_notify_email_generic, + getString(R.string.notification_new_title), + System.currentTimeMillis()); + notification.setLatestEventInfo(this, + getString(R.string.notification_new_title), + reportString, + pending); + + notification.sound = ringtone; + notification.defaults = vibrate + ? Notification.DEFAULT_LIGHTS | Notification.DEFAULT_VIBRATE + : Notification.DEFAULT_LIGHTS; + + NotificationManager notificationManager = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(NEW_MESSAGE_NOTIFICATION_ID, notification); + } } diff --git a/src/com/android/exchange/adapter/EmailSyncAdapter.java b/src/com/android/exchange/adapter/EmailSyncAdapter.java index a6e17b60b..3d98f15cd 100644 --- a/src/com/android/exchange/adapter/EmailSyncAdapter.java +++ b/src/com/android/exchange/adapter/EmailSyncAdapter.java @@ -472,7 +472,7 @@ public class EmailSyncAdapter extends AbstractSyncAdapter { Notification notif = new Notification(R.drawable.stat_notify_email_generic, mContext.getString(R.string.notification_new_title), System.currentTimeMillis()); - Intent i = MessageList.actionHandleAccountIntent(mContext, mAccount.mId, + Intent i = MessageList.actionHandleAccountIntent(mContext, mAccount.mId, -1, Mailbox.TYPE_INBOX); PendingIntent pi = PendingIntent.getActivity(mContext, 0, i, 0); notif.setLatestEventInfo(mContext,