diff --git a/CleanSpec.mk b/CleanSpec.mk index ebb255650..133cf331d 100644 --- a/CleanSpec.mk +++ b/CleanSpec.mk @@ -54,6 +54,7 @@ $(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/APPS/Email_intermedia $(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/com.android.emailcommon_intermediates) $(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/com.android.emailcommon_intermediates) $(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/com.android.emailcommon_intermediates) +$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/com.android.emailcommon_intermediates) # ************************************************ # NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST diff --git a/emailcommon/src/com/android/emailcommon/provider/EmailContent.java b/emailcommon/src/com/android/emailcommon/provider/EmailContent.java index b2e164c8c..9f0c855e0 100644 --- a/emailcommon/src/com/android/emailcommon/provider/EmailContent.java +++ b/emailcommon/src/com/android/emailcommon/provider/EmailContent.java @@ -97,6 +97,7 @@ public abstract class EmailContent { public static final String FIELD_COLUMN_NAME = "field"; public static final String ADD_COLUMN_NAME = "add"; + public static final String SET_COLUMN_NAME = "set"; // Newly created objects get this id public static final int NOT_SAVED = -1; diff --git a/emailcommon/src/com/android/emailcommon/service/AccountServiceProxy.java b/emailcommon/src/com/android/emailcommon/service/AccountServiceProxy.java index 55e381908..0bf0d146a 100644 --- a/emailcommon/src/com/android/emailcommon/service/AccountServiceProxy.java +++ b/emailcommon/src/com/android/emailcommon/service/AccountServiceProxy.java @@ -22,6 +22,8 @@ import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; +import java.util.List; + public class AccountServiceProxy extends ServiceProxy implements IAccountService { public static final String ACCOUNT_INTENT = "com.android.email.ACCOUNT_INTENT"; @@ -44,7 +46,7 @@ public class AccountServiceProxy extends ServiceProxy implements IAccountService } @Override - public void notifyLoginFailed(final long accountId) throws RemoteException { + public void notifyLoginFailed(final long accountId) { setTask(new ProxyTask() { public void run() throws RemoteException { mService.notifyLoginFailed(accountId); @@ -53,7 +55,7 @@ public class AccountServiceProxy extends ServiceProxy implements IAccountService } @Override - public void notifyLoginSucceeded(final long accountId) throws RemoteException { + public void notifyLoginSucceeded(final long accountId) { setTask(new ProxyTask() { public void run() throws RemoteException { mService.notifyLoginSucceeded(accountId); @@ -62,16 +64,17 @@ public class AccountServiceProxy extends ServiceProxy implements IAccountService } @Override - public void notifyNewMessages(final long accountId) throws RemoteException { + @SuppressWarnings("unchecked") + public void notifyNewMessages(final long accountId, final List messageIdList) { setTask(new ProxyTask() { public void run() throws RemoteException { - mService.notifyNewMessages(accountId); + mService.notifyNewMessages(accountId, messageIdList); } }, "notifyNewMessages"); } @Override - public void accountDeleted() throws RemoteException { + public void accountDeleted() { setTask(new ProxyTask() { public void run() throws RemoteException { mService.accountDeleted(); @@ -81,7 +84,7 @@ public class AccountServiceProxy extends ServiceProxy implements IAccountService // The following call is synchronous, and should not be made from the UI thread @Override - public void restoreAccountsIfNeeded() throws RemoteException { + public void restoreAccountsIfNeeded() { setTask(new ProxyTask() { public void run() throws RemoteException { mService.restoreAccountsIfNeeded(); @@ -92,7 +95,7 @@ public class AccountServiceProxy extends ServiceProxy implements IAccountService // The following call is synchronous, and should not be made from the UI thread @Override - public int getAccountColor(final long accountId) throws RemoteException { + public int getAccountColor(final long accountId) { setTask(new ProxyTask() { public void run() throws RemoteException{ mReturn = mService.getAccountColor(accountId); @@ -107,7 +110,7 @@ public class AccountServiceProxy extends ServiceProxy implements IAccountService } // The following call is synchronous, and should not be made from the UI thread - public Bundle getConfigurationData(final String accountType) throws RemoteException { + public Bundle getConfigurationData(final String accountType) { setTask(new ProxyTask() { public void run() throws RemoteException{ mReturn = mService.getConfigurationData(accountType); @@ -122,7 +125,7 @@ public class AccountServiceProxy extends ServiceProxy implements IAccountService } // The following call is synchronous, and should not be made from the UI thread - public String getDeviceId() throws RemoteException { + public String getDeviceId() { setTask(new ProxyTask() { public void run() throws RemoteException{ mReturn = mService.getDeviceId(); diff --git a/emailcommon/src/com/android/emailcommon/service/IAccountService.aidl b/emailcommon/src/com/android/emailcommon/service/IAccountService.aidl index ea945c04e..db8908c0c 100644 --- a/emailcommon/src/com/android/emailcommon/service/IAccountService.aidl +++ b/emailcommon/src/com/android/emailcommon/service/IAccountService.aidl @@ -21,7 +21,7 @@ import android.os.Bundle; interface IAccountService { oneway void notifyLoginFailed(long accountId); oneway void notifyLoginSucceeded(long accountId); - oneway void notifyNewMessages(long accountId); + oneway void notifyNewMessages(long accountId, in List messageIdList); void accountDeleted(); void restoreAccountsIfNeeded(); diff --git a/src/com/android/email/Controller.java b/src/com/android/email/Controller.java index ed130e316..b2fc76de1 100644 --- a/src/com/android/email/Controller.java +++ b/src/com/android/email/Controller.java @@ -58,6 +58,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.security.InvalidParameterException; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.concurrent.ConcurrentHashMap; @@ -1142,7 +1143,7 @@ public class Controller { * @param numNewMessages the number of new messages delivered */ public void updateMailboxCallback(MessagingException result, long accountId, - long mailboxId, int progress, int numNewMessages) { + long mailboxId, int progress, int numNewMessages, ArrayList addedMessages) { } /** @@ -1292,17 +1293,18 @@ public class Controller { public void synchronizeMailboxStarted(long accountId, long mailboxId) { synchronized (mListeners) { for (Result l : mListeners) { - l.updateMailboxCallback(null, accountId, mailboxId, 0, 0); + l.updateMailboxCallback(null, accountId, mailboxId, 0, 0, null); } } } @Override public void synchronizeMailboxFinished(long accountId, long mailboxId, - int totalMessagesInMailbox, int numNewMessages) { + int totalMessagesInMailbox, int numNewMessages, ArrayList addedMessages) { synchronized (mListeners) { for (Result l : mListeners) { - l.updateMailboxCallback(null, accountId, mailboxId, 100, numNewMessages); + l.updateMailboxCallback(null, accountId, mailboxId, 100, numNewMessages, + addedMessages); } } } @@ -1317,7 +1319,7 @@ public class Controller { } synchronized (mListeners) { for (Result l : mListeners) { - l.updateMailboxCallback(me, accountId, mailboxId, 0, 0); + l.updateMailboxCallback(me, accountId, mailboxId, 0, 0, null); } } } @@ -1566,7 +1568,7 @@ public class Controller { long accountId = mbx.mAccountKey; synchronized(mListeners) { for (Result listener : mListeners) { - listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0); + listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0, null); } } } diff --git a/src/com/android/email/ControllerResultUiThreadWrapper.java b/src/com/android/email/ControllerResultUiThreadWrapper.java index 1f1bf25c6..9a33b929f 100644 --- a/src/com/android/email/ControllerResultUiThreadWrapper.java +++ b/src/com/android/email/ControllerResultUiThreadWrapper.java @@ -19,6 +19,8 @@ package com.android.email; import com.android.email.Controller.Result; import com.android.emailcommon.mail.MessagingException; +import java.util.ArrayList; + import android.os.Handler; /** @@ -106,12 +108,13 @@ public class ControllerResultUiThreadWrapper extends Result { @Override public void updateMailboxCallback(final MessagingException result, final long accountId, - final long mailboxId, final int progress, final int numNewMessages) { + final long mailboxId, final int progress, final int numNewMessages, + final ArrayList addedMessages) { run(new Runnable() { public void run() { if (!isRegistered()) return; mWrappee.updateMailboxCallback(result, accountId, mailboxId, progress, - numNewMessages); + numNewMessages, addedMessages); } }); } diff --git a/src/com/android/email/GroupMessagingListener.java b/src/com/android/email/GroupMessagingListener.java index 06d162147..a7b87c288 100644 --- a/src/com/android/email/GroupMessagingListener.java +++ b/src/com/android/email/GroupMessagingListener.java @@ -20,6 +20,7 @@ import com.android.emailcommon.mail.MessagingException; import android.content.Context; +import java.util.ArrayList; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -80,10 +81,10 @@ public class GroupMessagingListener extends MessagingListener { @Override synchronized public void synchronizeMailboxFinished(long accountId, long mailboxId, - int totalMessagesInMailbox, int numNewMessages) { + int totalMessagesInMailbox, int numNewMessages, ArrayList addedMessages) { for (MessagingListener l : mListeners) { l.synchronizeMailboxFinished(accountId, mailboxId, - totalMessagesInMailbox, numNewMessages); + totalMessagesInMailbox, numNewMessages, addedMessages); } } diff --git a/src/com/android/email/MessagingController.java b/src/com/android/email/MessagingController.java index 4aea7bf15..f46a1d27d 100644 --- a/src/com/android/email/MessagingController.java +++ b/src/com/android/email/MessagingController.java @@ -18,7 +18,6 @@ package com.android.email; import com.android.email.mail.Sender; import com.android.email.mail.Store; -import com.android.email.mail.StoreSynchronizer; import com.android.emailcommon.Logging; import com.android.emailcommon.internet.MimeBodyPart; import com.android.emailcommon.internet.MimeHeader; @@ -330,20 +329,19 @@ public class MessagingController implements Runnable { mListeners.synchronizeMailboxStarted(account.mId, folder.mId); if ((folder.mFlags & Mailbox.FLAG_HOLDS_MAIL) == 0) { // We don't hold messages, so, nothing to synchronize - mListeners.synchronizeMailboxFinished(account.mId, folder.mId, 0, 0); + mListeners.synchronizeMailboxFinished(account.mId, folder.mId, 0, 0, null); return; } NotificationController nc = NotificationController.getInstance(mContext); try { processPendingActionsSynchronous(account); - StoreSynchronizer.SyncResults results; - // Select generic sync or store-specific sync - results = synchronizeMailboxGeneric(account, folder); + SyncResults results = synchronizeMailboxGeneric(account, folder); mListeners.synchronizeMailboxFinished(account.mId, folder.mId, results.mTotalMessages, - results.mNewMessages); + results.mAddedMessages.size(), + results.mAddedMessages); // Clear authentication notification for this account nc.cancelLoginFailedNotification(account.mId); } catch (MessagingException e) { @@ -409,17 +407,23 @@ public class MessagingController implements Runnable { * @return results of the sync pass * @throws MessagingException */ - private StoreSynchronizer.SyncResults synchronizeMailboxGeneric( + private SyncResults synchronizeMailboxGeneric( final EmailContent.Account account, final EmailContent.Mailbox folder) throws MessagingException { + /* + * A list of IDs for messages that were downloaded and did not have the seen flag set. + * This serves as the "true" new message count reported to the user via notification. + */ + final ArrayList unseenMessages = new ArrayList(); + Log.d(Logging.LOG_TAG, "*** synchronizeMailboxGeneric ***"); ContentResolver resolver = mContext.getContentResolver(); // 0. We do not ever sync DRAFTS or OUTBOX (down or up) if (folder.mType == Mailbox.TYPE_DRAFTS || folder.mType == Mailbox.TYPE_OUTBOX) { int totalMessages = EmailContent.count(mContext, folder.getUri(), null, null); - return new StoreSynchronizer.SyncResults(totalMessages, 0); + return new SyncResults(totalMessages, unseenMessages); } // 1. Get the message list from the local store and create an index of the uids @@ -474,7 +478,7 @@ public class MessagingController implements Runnable { || folder.mType == Mailbox.TYPE_DRAFTS) { if (!remoteFolder.exists()) { if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { - return new StoreSynchronizer.SyncResults(0, 0); + return new SyncResults(0, unseenMessages); } } } @@ -541,13 +545,6 @@ public class MessagingController implements Runnable { } // 8. Download basic info about the new/unloaded messages (if any) - /* - * A list of messages that were downloaded and which did not have the Seen flag set. - * This will serve to indicate the true "new" message count that will be reported to - * the user via notification. - */ - final ArrayList newMessages = new ArrayList(); - /* * 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. @@ -584,7 +581,7 @@ public class MessagingController implements Runnable { saveOrUpdate(localMessage, mContext); // Track the "new" ness of the downloaded message if (!message.isSet(Flag.SEEN)) { - newMessages.add(message); + unseenMessages.add(localMessage.mId); } } catch (MessagingException me) { Log.e(Logging.LOG_TAG, @@ -753,7 +750,7 @@ public class MessagingController implements Runnable { // 14. Clean up and report results remoteFolder.close(false); - return new StoreSynchronizer.SyncResults(remoteMessageCount, newMessages.size()); + return new SyncResults(remoteMessageCount, unseenMessages); } /** @@ -1960,4 +1957,20 @@ public class MessagingController implements Runnable { return description; } } + + /** Results of the latest synchronization. */ + private static class SyncResults { + /** The total # of messages in the folder */ + public final int mTotalMessages; + /** A list of new message IDs; must not be {@code null} */ + public final ArrayList mAddedMessages; + + public SyncResults(int totalMessages, ArrayList addedMessages) { + if (addedMessages == null) { + throw new IllegalArgumentException("addedMessages must not be null"); + } + mTotalMessages = totalMessages; + mAddedMessages = addedMessages; + } + } } diff --git a/src/com/android/email/MessagingListener.java b/src/com/android/email/MessagingListener.java index 8b5573f7d..3371614a8 100644 --- a/src/com/android/email/MessagingListener.java +++ b/src/com/android/email/MessagingListener.java @@ -20,6 +20,8 @@ import com.android.emailcommon.mail.MessagingException; import android.content.Context; +import java.util.ArrayList; + /** * Defines the interface that MessagingController will use to callback to requesters. This class * is defined as non-abstract so that someone who wants to receive only a few messages can @@ -40,16 +42,25 @@ public class MessagingListener { public void listFoldersFinished(long accountId) { } - public void synchronizeMailboxStarted(long accountId, long mailboxId) - { + public void synchronizeMailboxStarted(long accountId, long mailboxId) { } - public void synchronizeMailboxFinished(long accountId, - long mailboxId, int totalMessagesInMailbox, int numNewMessages) { + /** + * Synchronization of the mailbox finished. The mailbox and/or message databases have been + * updated accordingly. + * + * @param accountId The account that was synchronized + * @param mailboxId The mailbox that was synchronized + * @param totalMessagesInMailbox The total number of messages in the mailbox + * @param numNewMessages The number of new messages + * @param addedMessages Message IDs of messages that were added during the synchronization. + * These are new, unread messages. Messages that were previously read are not in this list. + */ + public void synchronizeMailboxFinished(long accountId, long mailboxId, + int totalMessagesInMailbox, int numNewMessages, ArrayList addedMessages) { } - public void synchronizeMailboxFailed(long accountId, long mailboxId, - Exception e) { + public void synchronizeMailboxFailed(long accountId, long mailboxId, Exception e) { } public void loadMessageForViewStarted(long messageId) { diff --git a/src/com/android/email/NotificationController.java b/src/com/android/email/NotificationController.java index 7a8c23d09..08e1aa1f8 100644 --- a/src/com/android/email/NotificationController.java +++ b/src/com/android/email/NotificationController.java @@ -23,6 +23,7 @@ import com.android.email.activity.setup.AccountSettingsXL; import com.android.emailcommon.mail.Address; import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.EmailContent.Account; +import com.android.emailcommon.provider.EmailContent.AccountColumns; import com.android.emailcommon.provider.EmailContent.Attachment; import com.android.emailcommon.provider.EmailContent.Message; import com.android.emailcommon.utility.EmailAsyncTask; @@ -32,16 +33,26 @@ import com.google.common.annotations.VisibleForTesting; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; import android.content.Context; import android.content.Intent; +import android.database.ContentObserver; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.AudioManager; import android.net.Uri; +import android.os.Handler; import android.text.SpannableString; import android.text.TextUtils; import android.text.style.TextAppearanceSpan; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.NoSuchElementException; + /** * Class that manages notifications. */ @@ -63,6 +74,12 @@ public class NotificationController { private final AudioManager mAudioManager; private final Bitmap mGenericSenderIcon; private final Clock mClock; + // TODO The service context used to create and manage the notification controller is NOT + // guaranteed to live forever. As such, we may lose the data in this structure. We should + // save / restore this data upon service termination / start. We'd also want to define + // the behaviour after a restart. + /** Maps account id to the message data */ + private final HashMap mNotificationMap; /** Constructor */ @VisibleForTesting @@ -74,6 +91,7 @@ public class NotificationController { mGenericSenderIcon = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_contact_picture); mClock = clock; + mNotificationMap = new HashMap(); } /** Singleton access */ @@ -97,11 +115,13 @@ public class NotificationController { * @param largeIcon A large icon. May be {@code null} * @param number A number to display using {@link Notification.Builder#setNumber(int)}. May * be {@code null}. + * @param enableAudio If {@code false}, do not play any sound. Otherwise, play sound according + * to the settings for the given account. * @return A {@link Notification} that can be sent to the notification service. */ private Notification createAccountNotification(Account account, String ticker, CharSequence title, String contentText, Intent intent, Bitmap largeIcon, - Integer number) { + Integer number, boolean enableAudio) { // Pending Intent PendingIntent pending = null; if (intent != null) { @@ -119,7 +139,10 @@ public class NotificationController { .setSmallIcon(R.drawable.stat_notify_email_generic) .setWhen(mClock.getTime()) .setTicker(ticker); - setupSoundAndVibration(builder, account); + + if (enableAudio) { + setupSoundAndVibration(builder, account); + } Notification notification = builder.getNotification(); return notification; @@ -137,8 +160,8 @@ public class NotificationController { */ private void showAccountNotification(Account account, String ticker, String title, String contentText, Intent intent, int notificationId) { - Notification notification = //nb.getNotification(); - createAccountNotification(account, ticker, title, contentText, intent, null, null); + Notification notification = createAccountNotification(account, ticker, title, contentText, + intent, null, null, true); mNotificationManager.notify(notificationId, notification); } @@ -165,16 +188,35 @@ public class NotificationController { * @param accountId The ID of the account to cancel for. If {@code -1}, "new message" * notifications for all accounts will be canceled. */ - public void cancelNewMessageNotification(long accountId) { + public void cancelNewMessageNotification(final long accountId) { if (accountId == -1) { - new Utility.ForEachAccount(mContext) { - @Override - protected void performAction(long accountId) { - cancelNewMessageNotification(accountId); - } - }.execute(); + for (long id : mNotificationMap.keySet()) { + cancelNewMessageNotification(id); + } } else { + MessageData data = mNotificationMap.remove(accountId); + if (data == null) { + // Not in map; nothing to do here + return; + } + // ensure we don't accidentally double-cancel a notification + final ContentObserver myObserver = data.mObserver; + data.mObserver = null; mNotificationManager.cancel(getNewMessageNotificationId(accountId)); + + // now do the database work + EmailAsyncTask.runAsyncParallel(new Runnable() { + @Override + public void run() { + ContentResolver resolver = mContext.getContentResolver(); + if (myObserver != null) { + resolver.unregisterContentObserver(myObserver); + } + Uri uri = Account.RESET_NEW_MESSAGE_COUNT_URI; + uri = ContentUris.withAppendedId(uri, accountId); + resolver.update(uri, null, null, null); + } + }); } } @@ -182,17 +224,58 @@ public class NotificationController { * Show (or update) a "new message" notification for the given account. * * @param accountId The ID of the account to display a notification for. - * @param unseenMessageCount The number of messages in the account that are unseen. + * @param addedMessages A list of new message IDs added to the given account. */ - public void showNewMessageNotification(final long accountId, final int unseenMessageCount, - final int justFetchedCount) { + public void showNewMessageNotification(final long accountId, + final ArrayList addedMessages) { + if (addedMessages == null || addedMessages.size() == 0) { + // No messages added; nothing to do here + return; + } + MessageData data = mNotificationMap.get(accountId); + if (data == null) { + data = new MessageData(); + mNotificationMap.put(accountId, data); + } + final HashSet idSet = data.mMessageList; + synchronized (idSet) { + idSet.addAll(addedMessages); + } + // Pick a message to observe + final long messageId = idSet.iterator().next(); + final ContentObserver myObserver; + if (data.mObserver == null) { + myObserver = new MessageContentObserver(Utility.getMainThreadHandler(), mContext, + accountId, messageId); + data.mObserver = myObserver; + } else { + myObserver = data.mObserver; + } + EmailAsyncTask.runAsyncParallel(new Runnable() { @Override public void run() { - Notification n = createNewMessageNotification(accountId, unseenMessageCount); + ContentResolver resolver = mContext.getContentResolver(); + // Atomically update the unseen count + ContentValues cv = new ContentValues(); + cv.put(EmailContent.FIELD_COLUMN_NAME, AccountColumns.NEW_MESSAGE_COUNT); + cv.put(EmailContent.ADD_COLUMN_NAME, addedMessages.size()); + Uri uri = ContentUris.withAppendedId(Account.ADD_TO_FIELD_URI, accountId); + resolver.update(uri, cv, null, null); + // Get the unseen count + uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); + int unseenMessageCount = Utility.getFirstRowInt(mContext, uri, + new String[] { AccountColumns.NEW_MESSAGE_COUNT }, null /*selection*/, + null /*selectionArgs*/, null /*sortOrder*/, 0 /*column*/, 0 /*default*/); + // Create the notification + Notification n = createNewMessageNotification(accountId, unseenMessageCount, true); if (n == null) { return; } + // Register a content observer with one of the messages + uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId); + resolver.registerContentObserver(uri, false, myObserver); + // Make the notification visible mNotificationManager.notify(getNewMessageNotificationId(accountId), n); } }); @@ -222,7 +305,8 @@ public class NotificationController { * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) */ @VisibleForTesting - Notification createNewMessageNotification(long accountId, int unseenMessageCount) { + Notification createNewMessageNotification(long accountId, int unseenMessageCount, + boolean enableAudio) { final Account account = Account.restoreAccountWithId(mContext, accountId); if (account == null) { return null; @@ -244,8 +328,8 @@ public class NotificationController { final Bitmap largeIcon = senderPhoto != null ? senderPhoto : mGenericSenderIcon; final Integer number = unseenMessageCount > 1 ? unseenMessageCount : null; - Notification notification = - createAccountNotification(account, null, title, subject, intent, largeIcon, number); + Notification notification = createAccountNotification(account, null, title, subject, + intent, largeIcon, number, enableAudio); return notification; } @@ -416,4 +500,119 @@ public class NotificationController { public void cancelSecurityNeededNotification() { cancelNotification(NOTIFICATION_ID_SECURITY_NEEDED); } + + /** + * Observer invoked whenever a message we're notifying the user about changes. + */ + private static class MessageContentObserver extends ContentObserver { + /** The account this observer is attached to */ + private final long mAccountId; + /** A singular message ID to notify on */ + private final long mMessageId; + /** The context */ + private final Context mContext; + /** The handler we will be invoked on */ + private final Handler mHandler; + + MessageContentObserver(Handler handler, Context context, long accountId, + long messageId) { + super (handler); + mHandler = handler; + mContext = context; + mAccountId = accountId; + mMessageId = messageId; + } + + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + final MessageData data = sInstance.mNotificationMap.get(mAccountId); + // If this account had been removed from the set of notifications or if the observer + // has been updated, make sure we don't get called again + if (data == null || data.mObserver != this) { + mContext.getContentResolver().unregisterContentObserver(this); + return; + } + + // Ensure we're only handling one change at a time + EmailAsyncTask.runAsyncSerial(new Runnable() { + @Override + public void run() { + handleChange(data); + } + }); + } + + /** + * Performs any database operations to handle an observed change. + * + * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) + * @param data Message data for the observed account + */ + private void handleChange(MessageData data) { + Message message = Message.restoreMessageWithId(mContext, mMessageId); + if (message != null && !message.mFlagRead) { + // do nothing; wait until this message is modified + return; + } + + // message removed or read; get another one in the list and update the notification + // Remove ourselves from the set of notifiers + ContentResolver resolver = mContext.getContentResolver(); + resolver.unregisterContentObserver(this); + synchronized (data.mMessageList) { + data.mMessageList.remove(mMessageId); + } + try { + for (;;) { + long nextMessageId = data.mMessageList.iterator().next(); + Message nextMessage = Message.restoreMessageWithId(mContext, nextMessageId); + if ((nextMessage == null) || (nextMessage.mFlagRead)) { + synchronized (data.mMessageList) { + data.mMessageList.remove(nextMessageId); + } + continue; + } + data.mObserver = new MessageContentObserver(mHandler, mContext, mAccountId, + nextMessageId); + Uri uri = ContentUris.withAppendedId( + EmailContent.Message.CONTENT_URI, nextMessageId); + resolver.registerContentObserver(uri, false, data.mObserver); + + // Update the new message count + int unseenMessageCount = data.mMessageList.size(); + ContentValues cv = new ContentValues(); + + cv.put(EmailContent.SET_COLUMN_NAME, unseenMessageCount); + uri = ContentUris.withAppendedId( + Account.RESET_NEW_MESSAGE_COUNT_URI, mAccountId); + resolver.update(uri, cv, null, null); + + // Re-display the notification w/o audio + Notification n = sInstance.createNewMessageNotification(mAccountId, + unseenMessageCount, false); + sInstance.mNotificationManager.notify( + sInstance.getNewMessageNotificationId(mAccountId), n); + break; + } + } catch (NoSuchElementException e) { + // this is not an error; it means the list is empty, so, hide the notification + mHandler.post(new Runnable() { + @Override + public void run() { + // make sure we're on the UI thread to cancel the notification + sInstance.cancelNewMessageNotification(mAccountId); + } + }); + } + } + } + + /** + * Information about the message(s) we're notifying the user about. + */ + private static class MessageData { + final HashSet mMessageList = new HashSet(); + ContentObserver mObserver; + } } diff --git a/src/com/android/email/RefreshManager.java b/src/com/android/email/RefreshManager.java index 3f20d9d9e..986a55dbd 100644 --- a/src/com/android/email/RefreshManager.java +++ b/src/com/android/email/RefreshManager.java @@ -385,7 +385,8 @@ public class RefreshManager { */ @Override public void updateMailboxCallback(MessagingException exception, long accountId, - long mailboxId, int progress, int dontUseNumNewMessages) { + long mailboxId, int progress, int dontUseNumNewMessages, + ArrayList addedMessages) { if (LOG_ENABLED) { Log.d(Logging.LOG_TAG, "updateMailboxCallback " + accountId + ", " + mailboxId + ", " + progress + ", " + exceptionToString(exception)); diff --git a/src/com/android/email/activity/AccountFolderList.java b/src/com/android/email/activity/AccountFolderList.java index 218af0f01..a30912e33 100644 --- a/src/com/android/email/activity/AccountFolderList.java +++ b/src/com/android/email/activity/AccountFolderList.java @@ -28,6 +28,8 @@ import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.EmailContent.Account; import com.android.emailcommon.provider.EmailContent.Mailbox; +import java.util.ArrayList; + import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; @@ -286,7 +288,8 @@ public class AccountFolderList extends Activity implements AccountFolderListFrag @Override public void updateMailboxCallback(MessagingException result, long accountKey, - long mailboxKey, int progress, int numNewMessages) { + long mailboxKey, int progress, int numNewMessages, + ArrayList addedMessages) { updateProgress(result, progress); } diff --git a/src/com/android/email/activity/AccountFolderListFragment.java b/src/com/android/email/activity/AccountFolderListFragment.java index 79537f19e..6fb28dc70 100644 --- a/src/com/android/email/activity/AccountFolderListFragment.java +++ b/src/com/android/email/activity/AccountFolderListFragment.java @@ -28,6 +28,8 @@ import com.android.emailcommon.provider.EmailContent.Message; import com.android.emailcommon.provider.EmailContent.MessageColumns; import com.android.emailcommon.utility.Utility; +import java.util.ArrayList; + import android.app.Activity; import android.app.ListFragment; import android.content.Context; @@ -448,7 +450,8 @@ public class AccountFolderListFragment extends ListFragment private class ControllerResults extends Controller.Result { @Override public void updateMailboxCallback(MessagingException result, long accountKey, - long mailboxKey, int progress, int numNewMessages) { + long mailboxKey, int progress, int numNewMessages, + ArrayList addedMessages) { if (progress == 100) { updateAccounts(); } diff --git a/src/com/android/email/activity/EmailActivity.java b/src/com/android/email/activity/EmailActivity.java index cd05b326d..c3fdee4b3 100644 --- a/src/com/android/email/activity/EmailActivity.java +++ b/src/com/android/email/activity/EmailActivity.java @@ -20,6 +20,7 @@ import com.android.email.Controller; import com.android.email.ControllerResultUiThreadWrapper; import com.android.email.Email; import com.android.email.MessagingExceptionStrings; +import com.android.email.NotificationController; import com.android.email.R; import com.android.emailcommon.Logging; import com.android.emailcommon.mail.MessagingException; @@ -51,6 +52,7 @@ import android.view.View; import android.widget.TextView; import java.security.InvalidParameterException; +import java.util.ArrayList; /** * The main Email activity, which is used on both the tablet and the phone. @@ -480,7 +482,7 @@ public class EmailActivity extends Activity implements View.OnClickListener { @Override public void updateMailboxCallback(MessagingException result, long accountId, long mailboxId, - int progress, int numNewMessages) { + int progress, int numNewMessages, ArrayList addedMessages) { handleError(result, accountId, progress); } diff --git a/src/com/android/email/activity/MailboxList.java b/src/com/android/email/activity/MailboxList.java index e206447f4..829daf197 100644 --- a/src/com/android/email/activity/MailboxList.java +++ b/src/com/android/email/activity/MailboxList.java @@ -28,6 +28,8 @@ import com.android.emailcommon.provider.EmailContent.Account; import com.android.emailcommon.provider.EmailContent.AccountColumns; import com.android.emailcommon.utility.Utility; +import java.util.ArrayList; + import android.app.ActionBar; import android.app.Activity; import android.content.ContentUris; @@ -288,7 +290,7 @@ public class MailboxList extends Activity implements MailboxListFragment.Callbac @Override public void updateMailboxCallback(MessagingException result, long accountKey, - long mailboxKey, int progress, int numNewMessages) { + long mailboxKey, int progress, int numNewMessages, ArrayList addedMessages) { if (accountKey == mAccountId) { updateBanner(result, progress); updateProgress(result, progress); diff --git a/src/com/android/email/mail/Store.java b/src/com/android/email/mail/Store.java index cb6d0f49e..e84cdcafd 100644 --- a/src/com/android/email/mail/Store.java +++ b/src/com/android/email/mail/Store.java @@ -242,14 +242,6 @@ public abstract class Store { return com.android.email.activity.setup.AccountSetupIncoming.class; } - /** - * Get class of sync'er for this Store class - * @return Message Sync controller, or null to use default - */ - public StoreSynchronizer getMessageSynchronizer() { - return null; - } - /** * Some stores cannot download a message based only on the uid, and need the message structure * to be preloaded and provided to them. This method allows a remote store to signal this diff --git a/src/com/android/email/mail/StoreSynchronizer.java b/src/com/android/email/mail/StoreSynchronizer.java deleted file mode 100644 index bf7746de3..000000000 --- a/src/com/android/email/mail/StoreSynchronizer.java +++ /dev/null @@ -1,77 +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.mail; - -import com.android.email.MessagingListener; -import com.android.emailcommon.mail.MessagingException; -import com.android.emailcommon.provider.EmailContent; -import com.android.email.GroupMessagingListener; - -import android.content.Context; - -/** - * This interface allows a store to define a completely different synchronizer algorithm, - * as necessary. - */ -public interface StoreSynchronizer { - - /** - * An object of this class is returned by SynchronizeMessagesSynchronous to report - * the results of the sync run. - */ - public static class SyncResults { - /** - * The total # of messages in the folder - */ - public int mTotalMessages; - /** - * The # of new messages in the folder - */ - public int mNewMessages; - - public SyncResults(int totalMessages, int newMessages) { - mTotalMessages = totalMessages; - mNewMessages = newMessages; - } - } - - /** - * The job of this method is to synchronize messages between a remote folder and the - * corresponding local folder. - * - * The following callbacks should be called during this operation: - * {@link MessagingListener#synchronizeMailboxNewMessage(Account, String, Message)} - * {@link MessagingListener#synchronizeMailboxRemovedMessage(Account, String, Message)} - * - * Callbacks (through listeners) *must* be synchronized on the listeners object, e.g. - * synchronized (listeners) { - * for(MessagingListener listener : listeners) { - * listener.synchronizeMailboxNewMessage(account, folder, message); - * } - * } - * - * @param account The account to synchronize - * @param folder The folder to synchronize - * @param listeners callbacks to make during sync operation - * @param context if needed for making system calls - * @return an object describing the sync results - */ - public SyncResults SynchronizeMessagesSynchronous( - EmailContent.Account account, EmailContent.Mailbox folder, - GroupMessagingListener listeners, Context context) throws MessagingException; - -} diff --git a/src/com/android/email/mail/store/ExchangeStore.java b/src/com/android/email/mail/store/ExchangeStore.java index ef118f3e3..15c55df1b 100644 --- a/src/com/android/email/mail/store/ExchangeStore.java +++ b/src/com/android/email/mail/store/ExchangeStore.java @@ -18,7 +18,6 @@ package com.android.email.mail.store; import com.android.email.ExchangeUtils; import com.android.email.mail.Store; -import com.android.email.mail.StoreSynchronizer; import com.android.emailcommon.mail.Folder; import com.android.emailcommon.mail.MessagingException; import com.android.emailcommon.provider.EmailContent.Account; @@ -83,18 +82,6 @@ public class ExchangeStore extends Store { return com.android.email.activity.setup.AccountSetupExchange.class; } - /** - * Get class of sync'er for this Store class. Because exchange Sync rules are so different - * than IMAP or POP3, it's likely that an Exchange implementation will need its own sync - * controller. If so, this function must return a non-null value. - * - * @return Message Sync controller, or null to use default - */ - @Override - public StoreSynchronizer getMessageSynchronizer() { - return null; - } - /** * Inform MessagingController that this store requires message structures to be prefetched * before it can fetch message bodies (this is due to EAS protocol restrictions.) diff --git a/src/com/android/email/provider/EmailProvider.java b/src/com/android/email/provider/EmailProvider.java index 67d0aad6d..e68514a82 100644 --- a/src/com/android/email/provider/EmailProvider.java +++ b/src/com/android/email/provider/EmailProvider.java @@ -1664,8 +1664,16 @@ public class EmailProvider extends ContentProvider { if (cache != null) { cache.lock(id); } + ContentValues newMessageCount = CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT; + if (values != null) { + Long set = values.getAsLong(EmailContent.SET_COLUMN_NAME); + if (set != null) { + newMessageCount = new ContentValues(); + newMessageCount.put(Account.NEW_MESSAGE_COUNT, set); + } + } try { - result = db.update(tableName, CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT, + result = db.update(tableName, newMessageCount, whereWithId(id, selection), selectionArgs); } finally { if (cache != null) { diff --git a/src/com/android/email/service/AccountService.java b/src/com/android/email/service/AccountService.java index 42725c276..1778c318a 100644 --- a/src/com/android/email/service/AccountService.java +++ b/src/com/android/email/service/AccountService.java @@ -25,16 +25,16 @@ import com.android.email.VendorPolicyLoader; import com.android.emailcommon.Configuration; import com.android.emailcommon.Device; import com.android.emailcommon.service.IAccountService; -import com.android.emailcommon.utility.Utility; +import com.android.emailcommon.utility.EmailAsyncTask; 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 java.io.IOException; +import java.util.List; public class AccountService extends Service { @@ -44,37 +44,38 @@ public class AccountService extends Service { private final IAccountService.Stub mBinder = new IAccountService.Stub() { @Override - public void notifyLoginFailed(long accountId) throws RemoteException { + public void notifyLoginFailed(long accountId) { NotificationController.getInstance(mContext).showLoginFailedNotification(accountId); } @Override - public void notifyLoginSucceeded(long accountId) throws RemoteException { + public void notifyLoginSucceeded(long accountId) { NotificationController.getInstance(mContext).cancelLoginFailedNotification(accountId); } @Override - public void notifyNewMessages(long accountId) throws RemoteException { - MailService.actionNotifyNewMessages(mContext, accountId); + @SuppressWarnings("unchecked") + public void notifyNewMessages(long accountId, List messageIdList) { + MailService.actionNotifyNewMessages(mContext, accountId, messageIdList); } @Override - public void restoreAccountsIfNeeded() throws RemoteException { + public void restoreAccountsIfNeeded() { AccountBackupRestore.restoreAccountsIfNeeded(mContext); } @Override - public void accountDeleted() throws RemoteException { + public void accountDeleted() { MailService.accountDeleted(mContext); } @Override - public int getAccountColor(long accountId) throws RemoteException { + public int getAccountColor(long accountId) { return ResourceHelper.getInstance(mContext).getAccountColor(accountId); } @Override - public Bundle getConfigurationData(String accountType) throws RemoteException { + public Bundle getConfigurationData(String accountType) { Bundle bundle = new Bundle(); bundle.putBoolean(Configuration.EXCHANGE_CONFIGURATION_USE_ALTERNATE_STRINGS, VendorPolicyLoader.getInstance(mContext).useAlternateExchangeStrings()); @@ -82,9 +83,9 @@ public class AccountService extends Service { } @Override - public String getDeviceId() throws RemoteException { + public String getDeviceId() { try { - Utility.runAsync(new Runnable() { + EmailAsyncTask.runAsyncSerial(new Runnable() { @Override public void run() { // Make sure the service is properly running (re: lifecycle) diff --git a/src/com/android/email/service/MailService.java b/src/com/android/email/service/MailService.java index d751f37e5..b9960bb5b 100644 --- a/src/com/android/email/service/MailService.java +++ b/src/com/android/email/service/MailService.java @@ -27,11 +27,11 @@ import com.android.emailcommon.AccountManagerTypes; import com.android.emailcommon.mail.MessagingException; import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.EmailContent.Account; -import com.android.emailcommon.provider.EmailContent.AccountColumns; import com.android.emailcommon.provider.EmailContent.HostAuth; import com.android.emailcommon.provider.EmailContent.Mailbox; import com.android.emailcommon.utility.AccountReconciler; -import com.android.emailcommon.utility.Utility; +import com.android.emailcommon.utility.EmailAsyncTask; +import com.google.common.annotations.VisibleForTesting; import android.accounts.AccountManager; import android.accounts.AccountManagerCallback; @@ -47,7 +47,6 @@ import android.database.Cursor; import android.net.ConnectivityManager; import android.net.Uri; import android.os.Bundle; -import android.os.Handler; import android.os.IBinder; import android.os.SystemClock; import android.text.TextUtils; @@ -56,6 +55,7 @@ import android.util.Log; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.logging.Logger; /** * Background service for refreshing non-push email accounts. @@ -82,24 +82,25 @@ public class MailService extends Service { 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"; + private static final String EXTRA_MESSAGE_ID_COUNT = + "com.android.email.intent.extra.MESSAGE_ID_COUNT"; + private static final String EXTRA_MESSAGE_ID_PREFIX = + "com.android.email.intent.extra.MESSAGE_ID_"; - private static final int WATCHDOG_DELAY = 10 * 60 * 1000; // 10 minutes + /** 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 /*package*/ static final int SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY = -1; // Sentinel value asking that mSyncReports be rebuilt /*package*/ static final int SYNC_REPORTS_RESET = -2; - private static final String[] NEW_MESSAGE_COUNT_PROJECTION = - new String[] {AccountColumns.NEW_MESSAGE_COUNT}; - private static MailService sMailService; /*package*/ Controller mController; private final Controller.Result mControllerCallback = new ControllerResults(); private ContentResolver mContentResolver; private Context mContext; - private Handler mHandler = new Handler(); private int mStartId; @@ -150,28 +151,7 @@ public class MailService extends Service { * @param accountId account to clear, or -1 for all accounts */ public static void resetNewMessageCount(final Context context, final long accountId) { - synchronized (mSyncReports) { - for (AccountSyncReport report : mSyncReports.values()) { - if (accountId == -1 || accountId == report.accountId) { - report.unseenMessageCount = 0; - report.lastUnseenMessageCount = 0; - } - } - } - // Clear notification NotificationController.getInstance(context).cancelNewMessageNotification(accountId); - - // now do the database - all accounts, or just one of them - Utility.runAsync(new Runnable() { - @Override - public void run() { - Uri uri = Account.RESET_NEW_MESSAGE_COUNT_URI; - if (accountId != -1) { - uri = ContentUris.withAppendedId(uri, accountId); - } - context.getContentResolver().update(uri, null, null, null); - } - }); } /** @@ -182,10 +162,21 @@ public class MailService extends Service { * @param context a context * @param accountId the id of the account that is reporting new messages */ - public static void actionNotifyNewMessages(Context context, long accountId) { + @SuppressWarnings("unchecked") + public static void actionNotifyNewMessages( + Context context, long accountId, List messageIdList) { Intent i = new Intent(ACTION_NOTIFY_MAIL); i.setClass(context, MailService.class); i.putExtra(EXTRA_ACCOUNT, accountId); + int listSize = 0; + if (messageIdList != null) { + listSize = messageIdList.size(); + for (int j = 0; j < listSize; j++) { + long messageId = (Long) messageIdList.get(j); + i.putExtra(EXTRA_MESSAGE_ID_PREFIX + j, messageId); + } + } + i.putExtra(EXTRA_MESSAGE_ID_COUNT, listSize); context.startService(i); } @@ -203,7 +194,7 @@ public class MailService extends Service { // Restore accounts, if it has not happened already AccountBackupRestore.restoreAccountsIfNeeded(this); - Utility.runAsync(new Runnable() { + EmailAsyncTask.runAsyncParallel(new Runnable() { @Override public void run() { reconcilePopImapAccountsSync(MailService.this); @@ -224,7 +215,7 @@ public class MailService extends Service { if (ACTION_CHECK_MAIL.equals(action)) { // DB access required to satisfy this intent, so offload from UI thread - Utility.runAsync(new Runnable() { + EmailAsyncTask.runAsyncParallel(new Runnable() { @Override public void run() { // If we have the data, restore the last-sync-times for each account @@ -280,7 +271,7 @@ public class MailService extends Service { if (Email.DEBUG) { Log.d(LOG_TAG, "action: delete exchange accounts"); } - Utility.runAsync(new Runnable() { + EmailAsyncTask.runAsyncParallel(new Runnable() { public void run() { Cursor c = mContentResolver.query(Account.CONTENT_URI, Account.ID_PROJECTION, null, null, null); @@ -304,7 +295,7 @@ public class MailService extends Service { if (Email.DEBUG) { Log.d(LOG_TAG, "action: send pending mail"); } - Utility.runAsync(new Runnable() { + EmailAsyncTask.runAsyncParallel(new Runnable() { public void run() { mController.sendPendingMessages(accountId); } @@ -315,17 +306,14 @@ public class MailService extends Service { if (Email.DEBUG) { Log.d(LOG_TAG, "action: reschedule"); } - final NotificationController nc = NotificationController.getInstance(this); // DB access required to satisfy this intent, so offload from UI thread - Utility.runAsync(new Runnable() { + EmailAsyncTask.runAsyncParallel(new Runnable() { @Override public void run() { // Clear all notifications, in case account list has changed. - // - // TODO Clear notifications for non-existing accounts. Now that we have - // separate notifications for each account, NotificationController should be - // able to do that. - nc.cancelNewMessageNotification(-1); + NotificationController + .getInstance(MailService.this) + .cancelNewMessageNotification(-1); // When called externally, we refresh the sync reports table to pick up // any changes in the account list or account settings @@ -337,23 +325,23 @@ public class MailService extends Service { }); } else if (ACTION_NOTIFY_MAIL.equals(action)) { // DB access required to satisfy this intent, so offload from UI thread - Utility.runAsync(new Runnable() { + EmailAsyncTask.runAsyncParallel(new Runnable() { @Override public void run() { - // Get the current new message count - Cursor c = mContentResolver.query( - ContentUris.withAppendedId(Account.CONTENT_URI, accountId), - NEW_MESSAGE_COUNT_PROJECTION, null, null, null); - int newMessageCount = 0; - try { - if (c.moveToFirst()) { - newMessageCount = c.getInt(0); - updateAccountReport(accountId, newMessageCount); - notifyNewMessages(accountId); + int newMessageCount = intent.getIntExtra(EXTRA_MESSAGE_ID_COUNT, 0); + ArrayList messageIdList = new ArrayList(); + for (int i = 0; i < newMessageCount; i++) { + final long messageId = + intent.getLongExtra(EXTRA_MESSAGE_ID_PREFIX + i, -1L); + if (messageId <= 0) { + // What else to do here?? This should never happen ... + Log.w(LOG_TAG, "invalid message id in notification; id: " + messageId); + continue; } - } finally { - c.close(); + messageIdList.add(messageId); } + updateAccountReport(accountId, newMessageCount); + notifyNewMessages(accountId, messageIdList); if (Email.DEBUG) { Log.d(LOG_TAG, "notify accountId=" + Long.toString(accountId) + " count=" + newMessageCount); @@ -404,10 +392,7 @@ public class MailService extends Service { AccountSyncReport oldReport = oldSyncReports.get(newReport.accountId); if (oldReport != null) { newReport.prevSyncTime = oldReport.prevSyncTime; - if (newReport.syncInterval > 0 && newReport.prevSyncTime != 0) { - newReport.nextSyncTime = - newReport.prevSyncTime + (newReport.syncInterval * 1000 * 60); - } + newReport.setNextSyncTime(); } } } @@ -533,38 +518,32 @@ public class MailService extends Service { * TODO: Look more closely at syncEnabled and see if we can simply coalesce it into * syncInterval (e.g. if !syncEnabled, set syncInterval to -1). */ - /*package*/ static class AccountSyncReport { + @VisibleForTesting + static class AccountSyncReport { long accountId; - long prevSyncTime; // 0 == unknown - long nextSyncTime; // 0 == ASAP -1 == don't sync - - /** # of "unseen" messages to show in notification */ - int unseenMessageCount; + /** 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}, show system notifications. */ + boolean notify; + /** If {@code true}, auto sync is enabled. */ + boolean syncEnabled; /** - * # of unseen, the value shown on the last notification. Used to - * calculate "the number of messages that have just been fetched". - * - * TODO It's a sort of cheating. Should we use the "real" number? The only difference - * is the first notification after reboot / process restart. + * Sets the next sync time using the previous sync time and sync interval. */ - int lastUnseenMessageCount; - - int syncInterval; - boolean notify; - - boolean syncEnabled; // whether auto sync is enabled for this account - - /** # of messages that have just been fetched */ - int getJustFetchedMessageCount() { - return unseenMessageCount - lastUnseenMessageCount; + void setNextSyncTime() { + if (syncInterval > 0 && prevSyncTime != 0) { + nextSyncTime = prevSyncTime + (syncInterval * 1000 * 60); + } } @Override public String toString() { - return "id=" + accountId - + " prevSync=" + prevSyncTime + " nextSync=" + nextSyncTime + " numUnseen=" - + unseenMessageCount; + return "id=" + accountId + " prevSync=" + prevSyncTime + " nextSync=" + nextSyncTime; } } @@ -644,8 +623,6 @@ public class MailService extends Service { report.accountId = account.mId; report.prevSyncTime = 0; report.nextSyncTime = (syncInterval > 0) ? 0 : -1; // 0 == ASAP -1 == no sync - report.unseenMessageCount = 0; - report.lastUnseenMessageCount = 0; report.syncInterval = syncInterval; report.notify = (account.mFlags & Account.FLAGS_NOTIFY_NEW_MAIL) != 0; @@ -685,12 +662,7 @@ public class MailService extends Service { // 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.unseenMessageCount = newCount; - } + report.setNextSyncTime(); if (Email.DEBUG) { Log.d(LOG_TAG, "update account " + report.toString()); } @@ -723,10 +695,7 @@ public class MailService extends Service { if (report != null) { if (report.prevSyncTime == 0) { report.prevSyncTime = prevSync; - if (report.syncInterval > 0 && report.prevSyncTime != 0) { - report.nextSyncTime = - report.prevSyncTime + (report.syncInterval * 1000 * 60); - } + report.setNextSyncTime(); } } } @@ -736,7 +705,8 @@ public class MailService extends Service { class ControllerResults extends Controller.Result { @Override public void updateMailboxCallback(MessagingException result, long accountId, - long mailboxId, int progress, int numNewMessages) { + long mailboxId, int progress, int numNewMessages, + ArrayList addedMessages) { // First, look for authentication failures and notify //checkAuthenticationStatus(result, accountId); if (result != null || progress == 100) { @@ -747,7 +717,7 @@ public class MailService extends Service { if (progress == 100) { updateAccountReport(accountId, numNewMessages); if (numNewMessages > 0) { - notifyNewMessages(accountId); + notifyNewMessages(accountId, addedMessages); } } else { updateAccountReport(accountId, -1); @@ -779,21 +749,16 @@ public class MailService extends Service { /** * Show "new message" notification for an account. (Notification is shown per account.) */ - private void notifyNewMessages(final long accountId) { - final int unseenMessageCount; - final int justFetchedCount; + private void notifyNewMessages(final long accountId, ArrayList addedMessages) { synchronized (mSyncReports) { AccountSyncReport report = mSyncReports.get(accountId); - if (report == null || report.unseenMessageCount == 0 || !report.notify) { + if (report == null || !report.notify) { return; } - unseenMessageCount = report.unseenMessageCount; - justFetchedCount = report.getJustFetchedMessageCount(); - report.lastUnseenMessageCount = report.unseenMessageCount; } - NotificationController.getInstance(this).showNewMessageNotification(accountId, - unseenMessageCount, justFetchedCount); + NotificationController.getInstance(this) + .showNewMessageNotification(accountId, addedMessages); } /** @@ -874,7 +839,8 @@ public class MailService extends Service { * @param blockExternalChanges FOR TESTING ONLY - block backups, security changes, etc. * @param resolver the content resolver for making provider updates (injected for testability) */ - /* package */ public static void reconcileAccountsWithAccountManager(Context context, + @VisibleForTesting + public static void reconcileAccountsWithAccountManager(Context context, List emailProviderAccounts, android.accounts.Account[] accountManagerAccounts, boolean blockExternalChanges, ContentResolver resolver) { boolean accountsDeleted = AccountReconciler.reconcileAccounts(context, diff --git a/tests/src/com/android/email/NotificationControllerTest.java b/tests/src/com/android/email/NotificationControllerTest.java index 2152362de..895c34dfc 100644 --- a/tests/src/com/android/email/NotificationControllerTest.java +++ b/tests/src/com/android/email/NotificationControllerTest.java @@ -213,7 +213,7 @@ public class NotificationControllerTest extends AndroidTestCase { Mailbox b1 = ProviderTestUtils.setupMailbox("inbox", a1.mId, true, c, Mailbox.TYPE_INBOX); Message m1 = ProviderTestUtils.setupMessage("message", a1.mId, b1.mId, true, true, c); - n = mTarget.createNewMessageNotification(a1.mId, 1); + n = mTarget.createNewMessageNotification(a1.mId, 1, true); assertEquals(R.drawable.stat_notify_email_generic, n.icon); assertEquals(mMockClock.mTime, n.when); @@ -223,7 +223,7 @@ public class NotificationControllerTest extends AndroidTestCase { // TODO Check content -- how? // Case 2: 1 account, 2 unseen message - n = mTarget.createNewMessageNotification(a1.mId, 2); + n = mTarget.createNewMessageNotification(a1.mId, 2, true); assertEquals(R.drawable.stat_notify_email_generic, n.icon); assertEquals(mMockClock.mTime, n.when); @@ -247,7 +247,7 @@ public class NotificationControllerTest extends AndroidTestCase { m1.save(c); // This shouldn't crash. - n = mTarget.createNewMessageNotification(a1.mId, 1); + n = mTarget.createNewMessageNotification(a1.mId, 1, true); // Minimum test for the result assertEquals(R.drawable.stat_notify_email_generic, n.icon); diff --git a/tests/src/com/android/email/RefreshManagerTest.java b/tests/src/com/android/email/RefreshManagerTest.java index 30fdd8c51..694a6e493 100644 --- a/tests/src/com/android/email/RefreshManagerTest.java +++ b/tests/src/com/android/email/RefreshManagerTest.java @@ -260,7 +260,7 @@ public class RefreshManagerTest extends InstrumentationTestCase { assertTrue(mTarget.isRefreshingAnyMessageListForTest()); // Refreshing mailbox 1... - mController.mListener.updateMailboxCallback(null, ACCOUNT_1, MAILBOX_1, 0, 0); + mController.mListener.updateMailboxCallback(null, ACCOUNT_1, MAILBOX_1, 0, 0, null); assertTrue(mListener.mCalledOnRefreshStatusChanged); assertFalse(mListener.mCalledOnConnectionError); @@ -272,7 +272,7 @@ public class RefreshManagerTest extends InstrumentationTestCase { // Done. Log.w(Logging.LOG_TAG, "" + mController.mListener.getClass()); - mController.mListener.updateMailboxCallback(null, ACCOUNT_1, MAILBOX_1, 100, 0); + mController.mListener.updateMailboxCallback(null, ACCOUNT_1, MAILBOX_1, 100, 0, null); assertTrue(mListener.mCalledOnRefreshStatusChanged); assertFalse(mListener.mCalledOnConnectionError); @@ -289,7 +289,7 @@ public class RefreshManagerTest extends InstrumentationTestCase { // Refreshing mailbox 2... mClock.advance(); - mController.mListener.updateMailboxCallback(null, ACCOUNT_2, MAILBOX_2, 0, 0); + mController.mListener.updateMailboxCallback(null, ACCOUNT_2, MAILBOX_2, 0, 0, null); assertTrue(mListener.mCalledOnRefreshStatusChanged); assertFalse(mListener.mCalledOnConnectionError); @@ -300,7 +300,7 @@ public class RefreshManagerTest extends InstrumentationTestCase { assertEquals(0, mTarget.getMessageListStatusForTest(MAILBOX_2).getLastRefreshTime()); // Done with exception. - mController.mListener.updateMailboxCallback(EXCEPTION, ACCOUNT_2, MAILBOX_2, 0, 0); + mController.mListener.updateMailboxCallback(EXCEPTION, ACCOUNT_2, MAILBOX_2, 0, 0, null); assertTrue(mListener.mCalledOnRefreshStatusChanged); assertTrue(mListener.mCalledOnConnectionError);