diff --git a/emailcommon/src/com/android/emailcommon/provider/EmailContent.java b/emailcommon/src/com/android/emailcommon/provider/EmailContent.java index 702ab8b47..513658ac2 100644 --- a/emailcommon/src/com/android/emailcommon/provider/EmailContent.java +++ b/emailcommon/src/com/android/emailcommon/provider/EmailContent.java @@ -619,21 +619,6 @@ public abstract class EmailContent { private static final String ACCOUNT_FAVORITE_SELECTION = ACCOUNT_KEY_SELECTION + " AND " + ALL_FAVORITE_SELECTION; - /** - * Selection for latest incoming messages. In order to tell whether incoming or not, - * we need the mailbox type, which is in the mailbox table, not the message table, so - * use a subquery. - */ - private static final String LATEST_INCOMING_MESSAGE_SELECTION = - MessageColumns.MAILBOX_KEY + " IN (SELECT " + RECORD_ID + " FROM " + Mailbox.TABLE_NAME - + " WHERE " + MailboxColumns.ACCOUNT_KEY + "=? AND " - + Mailbox.USER_VISIBLE_MAILBOX_SELECTION + " AND " - + MailboxColumns.TYPE + " NOT IN (" - + Mailbox.TYPE_DRAFTS + "," - + Mailbox.TYPE_OUTBOX + "," - + Mailbox.TYPE_SENT - + "))"; - // _id field is in AbstractContent public String mDisplayName; public long mTimeStamp; @@ -924,26 +909,6 @@ public abstract class EmailContent { } return -1; } - - /** - * @return the latest messages on an account. - */ - public static Message getLatestIncomingMessage(Context context, Long accountId) { - Cursor c = context.getContentResolver().query(Message.CONTENT_URI_LIMIT_1, - Message.CONTENT_PROJECTION, - LATEST_INCOMING_MESSAGE_SELECTION, new String[] {Long.toString(accountId)}, - EmailContent.MessageColumns.TIMESTAMP + " DESC"); - try { - if (c.moveToFirst()) { - Message m = new Message(); - m.restore(c); - return m; - } - } finally { - c.close(); - } - return null; // not found; - } } public interface AccountColumns { diff --git a/emailcommon/src/com/android/emailcommon/utility/Utility.java b/emailcommon/src/com/android/emailcommon/utility/Utility.java index c56b8be11..a21193b43 100644 --- a/emailcommon/src/com/android/emailcommon/utility/Utility.java +++ b/emailcommon/src/com/android/emailcommon/utility/Utility.java @@ -946,13 +946,20 @@ public class Utility { new String[] { Long.toString(mailbox.mId) }, MessageColumns.ID + " DESC", EmailContent.ID_PROJECTION_COLUMN, 0L); - ContentValues values = mailbox.toContentValues(); - values.put(MailboxColumns.LAST_SEEN_MESSAGE_KEY, messageId); - resolver.update( - Mailbox.CONTENT_URI, - values, - EmailContent.ID_SELECTION, - new String[] { Long.toString(mailbox.mId) }); + long oldLastSeenMessageId = Utility.getFirstRowLong( + context, ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailbox.mId), + new String[] { MailboxColumns.LAST_SEEN_MESSAGE_KEY }, + null, null, null, 0, 0L); + // Only update the db if the value has changed + if (messageId != oldLastSeenMessageId) { + ContentValues values = mailbox.toContentValues(); + values.put(MailboxColumns.LAST_SEEN_MESSAGE_KEY, messageId); + resolver.update( + Mailbox.CONTENT_URI, + values, + EmailContent.ID_SELECTION, + new String[] { Long.toString(mailbox.mId) }); + } } } diff --git a/src/com/android/email/NotificationController.java b/src/com/android/email/NotificationController.java index 08e1aa1f8..86b5d3d8f 100644 --- a/src/com/android/email/NotificationController.java +++ b/src/com/android/email/NotificationController.java @@ -20,13 +20,16 @@ import com.android.email.activity.ContactStatusLoader; import com.android.email.activity.Welcome; import com.android.email.activity.setup.AccountSecurity; import com.android.email.activity.setup.AccountSettingsXL; +import com.android.emailcommon.Logging; 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.Mailbox; +import com.android.emailcommon.provider.EmailContent.MailboxColumns; import com.android.emailcommon.provider.EmailContent.Message; -import com.android.emailcommon.utility.EmailAsyncTask; +import com.android.emailcommon.provider.EmailContent.MessageColumns; +import com.android.emailcommon.provider.ProviderUnavailableException; import com.android.emailcommon.utility.Utility; import com.google.common.annotations.VisibleForTesting; @@ -34,24 +37,23 @@ 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.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.AudioManager; import android.net.Uri; import android.os.Handler; +import android.os.Looper; +import android.os.Process; import android.text.SpannableString; import android.text.TextUtils; import android.text.style.TextAppearanceSpan; +import android.util.Log; -import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; -import java.util.NoSuchElementException; /** * Class that manages notifications. @@ -68,16 +70,27 @@ public class NotificationController { private static final int NOTIFICATION_ID_BASE_NEW_MESSAGES = 0x10000000; private static final int NOTIFICATION_ID_BASE_LOGIN_WARNING = 0x20000000; + /** Selection to retrieve accounts that should we notify user for changes */ + private final static String NOTIFIED_ACCOUNT_SELECTION = + Account.FLAGS + "&" + Account.FLAGS_NOTIFY_NEW_MAIL + " != 0"; + /** special account ID for the new message notification APIs to specify "all accounts" */ + private static final long ALL_ACCOUNTS = -1L; + /** Index into notification table returned from system preferences */ + private static final int NOTIFIED_MESSAGE_ID_INDEX = 0; + /** Index into notification table returned from system preferences */ + private static final int NOTIFIED_MESSAGE_COUNT_INDEX = 1; + + private static NotificationThread sNewMessageThread; + private static Handler sNewMessageHandler; private static NotificationController sInstance; private final Context mContext; private final NotificationManager mNotificationManager; 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. + // TODO We're maintaining all of our structures based upon the account ID. This is fine + // for now since the assumption is that we only ever look for changes in an account's + // INBOX. We should adjust our logic to use the mailbox ID instead. /** Maps account id to the message data */ private final HashMap mNotificationMap; @@ -165,15 +178,6 @@ public class NotificationController { mNotificationManager.notify(notificationId, notification); } - /** - * Cancels the specified notification. - * - * @param notificationId The ID of the notification to register with the service. - */ - private void cancelNotification(int notificationId) { - mNotificationManager.cancel(notificationId); - } - /** * Returns a notification ID for new message notifications for the given account. */ @@ -183,100 +187,201 @@ public class NotificationController { } /** - * Cancels a "new message" notification for the specified account. - * - * @param accountId The ID of the account to cancel for. If {@code -1}, "new message" - * notifications for all accounts will be canceled. + * Tells the notification controller if it should be watching for changes to the message table. + * This is the main life cycle method for message notifications. When we stop observing + * database changes, we save the state [e.g. message ID and count] of the most recent + * notification shown to the user. And, when we start observing database changes, we restore + * the saved state. + * @param watch If {@code true}, we register observers for all accounts whose settings have + * notifications enabled. Otherwise, all observers are unregistered with the database. */ - public void cancelNewMessageNotification(final long accountId) { - if (accountId == -1) { - for (long id : mNotificationMap.keySet()) { - cancelNewMessageNotification(id); + public void watchForMessages(final boolean watch) { + // Don't create the thread if we're only going to stop watching + if (!watch && sNewMessageHandler == null) return; + + synchronized(sInstance) { + if (sNewMessageHandler == null) { + sNewMessageThread = new NotificationThread(); + sNewMessageHandler = new Handler(sNewMessageThread.getLooper()); + } + } + // Run this on the message notification handler + sNewMessageHandler.post(new Runnable() { + @Override + public void run() { + ContentResolver resolver = mContext.getContentResolver(); + HashMap table; + if (!watch) { + table = new HashMap(); + for (Long key : mNotificationMap.keySet()) { + MessageData data = mNotificationMap.get(key); + table.put(key, + new long[] { data.mNotifiedMessageId, data.mNotifiedMessageCount }); + } + Preferences.getPreferences(mContext).setMessageNotificationTable(table); + unregisterMessageNotification(ALL_ACCOUNTS); + // TODO cancel existing account observers + + // tear down the event loop + sNewMessageThread.quit(); + sNewMessageHandler = null; + sNewMessageThread = null; + return; + } + + // otherwise, start new observers for all notified accounts + registerMessageNotification(ALL_ACCOUNTS); + // Need to load preferences _after_ starting the notifications. Otherwise, the + // notification map will not be built. + table = Preferences.getPreferences(mContext).getMessageNotificationTable(); + for (Long key : table.keySet()) { + MessageData data = mNotificationMap.get(key); + if (data != null) { + long[] value = table.get(key); + + data.mNotifiedMessageId = value[NOTIFIED_MESSAGE_ID_INDEX]; + data.mNotifiedMessageCount = (int) value[NOTIFIED_MESSAGE_COUNT_INDEX]; + } + } + // Loop through the observers and fire them once + for (MessageData data : mNotificationMap.values()) { + if (data.mObserver != null) { + data.mObserver.onChange(true); + } + } + // TODO Add an account observer to track when an account is removed + // TODO Add an account observer to track when an account is added + } + }); + } + + /** + * Registers an observer for changes to the INBOX for the given account. Since accounts + * may only have a single INBOX, we will never have more than one observer for an account. + * NOTE: This must be called on the notification handler thread. + * @param accountId The ID of the account to register the observer for. May be + * {@link #ALL_ACCOUNTS} to register observers for all accounts that allow + * for user notification. + */ + private void registerMessageNotification(long accountId) { + ContentResolver resolver = mContext.getContentResolver(); + if (accountId == ALL_ACCOUNTS) { + Cursor c = resolver.query( + Account.CONTENT_URI, EmailContent.ID_PROJECTION, + NOTIFIED_ACCOUNT_SELECTION, null, null); + try { + while (c.moveToNext()) { + long id = c.getLong(EmailContent.ID_PROJECTION_COLUMN); + registerMessageNotification(id); + } + } finally { + c.close(); } } 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)); + MessageData data = mNotificationMap.get(accountId); + if (data != null) return; // we're already observing; nothing to do - // 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); - } - }); + data = new MessageData(); + Mailbox mailbox = Mailbox.restoreMailboxOfType(mContext, accountId, Mailbox.TYPE_INBOX); + ContentObserver observer = new MessageContentObserver( + sNewMessageHandler, mContext, mailbox.mId, accountId); + resolver.registerContentObserver(Message.NOTIFIER_URI, true, observer); + data.mObserver = observer; + mNotificationMap.put(accountId, data); } } /** - * Show (or update) a "new message" notification for the given account. - * - * @param accountId The ID of the account to display a notification for. - * @param addedMessages A list of new message IDs added to the given account. + * Unregisters the observer for the given account. If the specified account does not have + * a registered observer, no action is performed. + * NOTE: This must be called on the notification handler thread. + * @param accountId The ID of the account to unregister from. May be {@link #ALL_ACCOUNTS} to + * unregister all accounts that have observers. */ - 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; + private void unregisterMessageNotification(long accountId) { + ContentResolver resolver = mContext.getContentResolver(); + if (accountId == ALL_ACCOUNTS) { + // cancel all existing message observers + for (MessageData data : mNotificationMap.values()) { + ContentObserver observer = data.mObserver; + resolver.unregisterContentObserver(observer); + } + mNotificationMap.clear(); } else { - myObserver = data.mObserver; + MessageData data = mNotificationMap.remove(accountId); + if (data != null) { + ContentObserver observer = data.mObserver; + resolver.unregisterContentObserver(observer); + } + } + } + + /** + * Cancels a "new message" notification for the specified account. + * + * @param accountId The ID of the account to cancel for. If {@link #ALL_ACCOUNTS}, "new message" + * notifications for all accounts will be canceled. + * @deprecated Components should not be invoking the notification controller directly. + */ + @Deprecated + public void cancelNewMessageNotification(final long accountId) { + synchronized(sInstance) { + // If we don't have a handler, we'll figure out the correct accounts to + // notify the next time we start the controller. + if (sNewMessageHandler == null) return; } - EmailAsyncTask.runAsyncParallel(new Runnable() { + // Run this on the message notification handler + sNewMessageHandler.post(new Runnable() { + private void clearNotification(long accountId) { + if (accountId == ALL_ACCOUNTS) { + for (long id : mNotificationMap.keySet()) { + clearNotification(id); + } + } else { + unregisterMessageNotification(accountId); + mNotificationManager.cancel(getNewMessageNotificationId(accountId)); + } + } @Override public void run() { - 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; + clearNotification(accountId); + } + }); + } + + /** + * Reset the message notification for the given account to the most recent message ID. + * This is not complete and will still exhibit the existing (wrong) behaviour of notifying + * the user even if the Email UX is visible. + * NOTE: Only for short-term legacy support. To be replaced with a way to temporarily stop + * notifications for a mailbox. + * @deprecated + */ + @Deprecated + public void resetMessageNotification(final long accountId) { + synchronized(sInstance) { + if (sNewMessageHandler == null) { + sNewMessageThread = new NotificationThread(); + sNewMessageHandler = new Handler(sNewMessageThread.getLooper()); + } + } + + // Run this on the message notification handler + sNewMessageHandler.post(new Runnable() { + private void resetNotification(long accountId) { + if (accountId == ALL_ACCOUNTS) { + for (long id : mNotificationMap.keySet()) { + resetNotification(id); + } + } else { + Utility.updateLastSeenMessageKey(mContext, accountId); + mNotificationManager.cancel(getNewMessageNotificationId(accountId)); } - // 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); + } + @Override + public void run() { + resetNotification(accountId); } }); } @@ -305,14 +410,14 @@ public class NotificationController { * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) */ @VisibleForTesting - Notification createNewMessageNotification(long accountId, int unseenMessageCount, - boolean enableAudio) { + Notification createNewMessageNotification(long accountId, long messageId, + int unseenMessageCount, boolean enableAudio) { final Account account = Account.restoreAccountWithId(mContext, accountId); if (account == null) { return null; } // Get the latest message - final Message message = Message.getLatestIncomingMessage(mContext, accountId); + final Message message = Message.restoreMessageWithId(mContext, messageId); if (message == null) { return null; // no message found??? } @@ -476,8 +581,8 @@ public class NotificationController { * Cancels any password expire notifications [both expired & expiring]. */ public void cancelPasswordExpirationNotifications() { - cancelNotification(NOTIFICATION_ID_PASSWORD_EXPIRING); - cancelNotification(NOTIFICATION_ID_PASSWORD_EXPIRED); + mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRING); + mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRED); } /** @@ -498,121 +603,131 @@ public class NotificationController { * Cancels the security needed notification. */ public void cancelSecurityNeededNotification() { - cancelNotification(NOTIFICATION_ID_SECURITY_NEEDED); + mNotificationManager.cancel(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 */ + /** A selection to get messages the user hasn't seen before */ + private final static String MESSAGE_SELECTION = + MessageColumns.MAILBOX_KEY + "=? AND " + MessageColumns.ID + ">? AND " + + MessageColumns.FLAG_READ + "=0"; private final Context mContext; - /** The handler we will be invoked on */ - private final Handler mHandler; + private final long mMailboxId; + private final long mAccountId; - MessageContentObserver(Handler handler, Context context, long accountId, - long messageId) { - super (handler); - mHandler = handler; + public MessageContentObserver( + Handler handler, Context context, long mailboxId, long accountId) { + super(handler); mContext = context; + mMailboxId = mailboxId; 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); + + MessageData data = sInstance.mNotificationMap.get(mAccountId); + if (data == null) { + // notification for a mailbox that we aren't observing; this should not happen + Log.e(Logging.LOG_TAG, "Received notifiaction when observer data was null"); return; } + long oldMessageId = data.mNotifiedMessageId; + int oldMessageCount = data.mNotifiedMessageCount; - // 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); - } + long lastSeenMessageId = Utility.getFirstRowLong( + mContext, Mailbox.CONTENT_URI, + new String[] { MailboxColumns.LAST_SEEN_MESSAGE_KEY }, + EmailContent.ID_SELECTION, + new String[] { Long.toString(mMailboxId) }, null, 0, 0L); + Cursor c = resolver.query( + Message.CONTENT_URI, EmailContent.ID_PROJECTION, + MESSAGE_SELECTION, + new String[] { Long.toString(mMailboxId), Long.toString(lastSeenMessageId) }, + MessageColumns.ID + " DESC"); + if (c == null) throw new ProviderUnavailableException(); 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; + int newMessageCount = c.getCount(); + long newMessageId = 0L; + if (c.moveToNext()) { + newMessageId = c.getLong(EmailContent.ID_PROJECTION_COLUMN); } - } 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); + + if (newMessageCount == 0) { + // No messages to notify for; clear the notification + sInstance.mNotificationManager.cancel( + sInstance.getNewMessageNotificationId(mAccountId)); + } else if (newMessageCount != oldMessageCount + || (newMessageId != 0 && newMessageId != oldMessageId)) { + // Either the count or last message has changed; update the notification + boolean playAudio = (oldMessageCount == 0); // play audio on first notification + Notification n = sInstance.createNewMessageNotification( + mAccountId, newMessageId, newMessageCount, playAudio); + if (n != null) { + // Make the notification visible + sInstance.mNotificationManager.notify( + sInstance.getNewMessageNotificationId(mAccountId), n); } - }); + } + data.mNotifiedMessageId = newMessageId; + data.mNotifiedMessageCount = newMessageCount; + } finally { + c.close(); } } } + /** Information about the message(s) we're notifying the user about. */ + private static class MessageData { + /** The database observer */ + ContentObserver mObserver; + /** Message ID used in the user notification */ + long mNotifiedMessageId; + /** Message count used in the user notification */ + int mNotifiedMessageCount; + } + /** - * Information about the message(s) we're notifying the user about. + * Thread to handle all notification actions through its own {@link Looper}. */ - private static class MessageData { - final HashSet mMessageList = new HashSet(); - ContentObserver mObserver; + private static class NotificationThread implements Runnable { + /** Lock to ensure proper initialization */ + private final Object mLock = new Object(); + /** The {@link Looper} that handles messages for this thread */ + private Looper mLooper; + + NotificationThread() { + new Thread(null, this, "EmailNotification").start(); + synchronized (mLock) { + while (mLooper == null) { + try { + mLock.wait(); + } catch (InterruptedException ex) { + } + } + } + } + + @Override + public void run() { + synchronized (mLock) { + Looper.prepare(); + mLooper = Looper.myLooper(); + mLock.notifyAll(); + } + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + Looper.loop(); + } + void quit() { + mLooper.quit(); + } + Looper getLooper() { + return mLooper; + } } } diff --git a/src/com/android/email/activity/AccountFolderList.java b/src/com/android/email/activity/AccountFolderList.java index a30912e33..ad87b36c3 100644 --- a/src/com/android/email/activity/AccountFolderList.java +++ b/src/com/android/email/activity/AccountFolderList.java @@ -204,6 +204,8 @@ public class AccountFolderList extends Activity implements AccountFolderListFrag public void onClick(DialogInterface dialog, int whichButton) { final long accountId = mSelectedContextAccount.mId; dismissDialog(DIALOG_REMOVE_ACCOUNT); + // TODO Do this automatically in the NotificationController as part of a + // ContentObserver // Dismiss new message notification. NotificationController.getInstance(activity) .cancelNewMessageNotification(accountId); diff --git a/src/com/android/email/activity/MessageListFragment.java b/src/com/android/email/activity/MessageListFragment.java index 02426e43d..1afaa8c2f 100644 --- a/src/com/android/email/activity/MessageListFragment.java +++ b/src/com/android/email/activity/MessageListFragment.java @@ -1200,6 +1200,8 @@ public class MessageListFragment extends ListFragment *
  • If {@code mailboxId} is not of a magic inbox (i.e. >= 0) and {@code * accountId} is valid, reset the count of the specified account. * + * TODO Instead of resetting the message count, we should just be suspending the notification + * controller for the correct accounts. Need to ensure we resume notifications appropriately. */ private static void resetNewMessageCount( Context context, long mailboxId, long accountId) { diff --git a/src/com/android/email/activity/setup/AccountSettingsXL.java b/src/com/android/email/activity/setup/AccountSettingsXL.java index 1558f184e..41bbdb504 100644 --- a/src/com/android/email/activity/setup/AccountSettingsXL.java +++ b/src/com/android/email/activity/setup/AccountSettingsXL.java @@ -664,6 +664,8 @@ public class AccountSettingsXL extends PreferenceActivity { public void deleteAccount(Account account) { // Kick off the work to actually delete the account + // TODO Do this automatically in the NotificationController as part of a + // ContentObserver // Clear notifications for the account. NotificationController.getInstance(this).cancelNewMessageNotification(account.mId); diff --git a/src/com/android/email/service/MailService.java b/src/com/android/email/service/MailService.java index 6fc65a7ba..853cb42a2 100644 --- a/src/com/android/email/service/MailService.java +++ b/src/com/android/email/service/MailService.java @@ -149,8 +149,9 @@ public class MailService extends Service { * * @param accountId account to clear, or -1 for all accounts */ + @SuppressWarnings("deprecation") public static void resetNewMessageCount(final Context context, final long accountId) { - NotificationController.getInstance(context).cancelNewMessageNotification(accountId); + NotificationController.getInstance(context).resetMessageNotification(accountId); } /** @@ -211,6 +212,7 @@ public class MailService extends Service { mContext = this; final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + NotificationController.getInstance(mContext).watchForMessages(true); if (ACTION_CHECK_MAIL.equals(action)) { // DB access required to satisfy this intent, so offload from UI thread @@ -309,11 +311,6 @@ public class MailService extends Service { EmailAsyncTask.runAsyncParallel(new Runnable() { @Override public void run() { - // Clear all notifications, in case account list has changed. - 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 refreshSyncReports(); @@ -323,31 +320,10 @@ public class MailService extends Service { } }); } else if (ACTION_NOTIFY_MAIL.equals(action)) { - // DB access required to satisfy this intent, so offload from UI thread - EmailAsyncTask.runAsyncParallel(new Runnable() { - @Override - public void run() { - 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; - } - messageIdList.add(messageId); - } - updateAccountReport(accountId, newMessageCount); - notifyNewMessages(accountId, messageIdList); - if (Email.DEBUG) { - Log.d(LOG_TAG, "notify accountId=" + Long.toString(accountId) - + " count=" + newMessageCount); - } - stopSelf(startId); - } - }); + // No need to do anything. For now, we need to tickle the MailService so it starts + // up the notification controller. It may make sense to put the notification controller + // in a place where it can live longer. + stopSelf(startId); } // Returning START_NOT_STICKY means that if a mail check is killed (e.g. due to memory @@ -365,6 +341,7 @@ public class MailService extends Service { @Override public void onDestroy() { super.onDestroy(); + NotificationController.getInstance(mContext).watchForMessages(false); Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback); } @@ -715,9 +692,6 @@ public class MailService extends Service { if (mailboxId == inboxId) { if (progress == 100) { updateAccountReport(accountId, numNewMessages); - if (numNewMessages > 0) { - notifyNewMessages(accountId, addedMessages); - } } else { updateAccountReport(accountId, -1); } @@ -745,21 +719,6 @@ public class MailService extends Service { } } - /** - * Show "new message" notification for an account. (Notification is shown per account.) - */ - private void notifyNewMessages(final long accountId, ArrayList addedMessages) { - synchronized (mSyncReports) { - AccountSyncReport report = mSyncReports.get(accountId); - if (report == null || !report.notify) { - return; - } - } - - NotificationController.getInstance(this) - .showNewMessageNotification(accountId, addedMessages); - } - /** * @see ConnectivityManager#getBackgroundDataSetting() */ diff --git a/tests/src/com/android/email/NotificationControllerTest.java b/tests/src/com/android/email/NotificationControllerTest.java index 895c34dfc..c6f25fbfe 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, true); + n = mTarget.createNewMessageNotification(a1.mId, m1.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, true); + n = mTarget.createNewMessageNotification(a1.mId, m1.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, true); + n = mTarget.createNewMessageNotification(a1.mId, m1.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/provider/ProviderTests.java b/tests/src/com/android/email/provider/ProviderTests.java index d5322d893..05863c5cd 100644 --- a/tests/src/com/android/email/provider/ProviderTests.java +++ b/tests/src/com/android/email/provider/ProviderTests.java @@ -1885,47 +1885,6 @@ public class ProviderTests extends ProviderTestCase2 { assertEquals(0, Account.restoreAccountWithId(c, a5.mId).mNewMessageCount); } - private static Message createMessageWithTimestamp(Context c, Mailbox b, long timestamp) { - Message m = ProviderTestUtils.setupMessage("1", b.mAccountKey, b.mId, true, false, c, false, - false); - m.mTimeStamp = timestamp; - m.save(c); - return m; - } - - public void testMessageGetLatestIncomingMessage() { - final Context c = mMockContext; - - // Create 2 accounts with a inbox. - Account a1 = ProviderTestUtils.setupAccount("a1", true, c); - Account a2 = ProviderTestUtils.setupAccount("a2", true, c); - - Mailbox b1 = ProviderTestUtils.setupMailbox("box1", a1.mId, true, c, Mailbox.TYPE_INBOX); - Mailbox b1d = ProviderTestUtils.setupMailbox("box1d", a1.mId, true, c, Mailbox.TYPE_DRAFTS); - Mailbox b1o = ProviderTestUtils.setupMailbox("box1o", a1.mId, true, c, Mailbox.TYPE_OUTBOX); - Mailbox b1s = ProviderTestUtils.setupMailbox("box1s", a1.mId, true, c, Mailbox.TYPE_SENT); - Mailbox b2 = ProviderTestUtils.setupMailbox("box2", a2.mId, true, c, Mailbox.TYPE_MAIL); - - // Create some messages - Message m11 = createMessageWithTimestamp(c, b1, 33); - Message m12 = createMessageWithTimestamp(c, b1, 10); - Message m13 = createMessageWithTimestamp(c, b1, 1000); // latest incoming - Message m1d = createMessageWithTimestamp(c, b1d, 2000); - Message m1o = createMessageWithTimestamp(c, b1o, 2000); - Message m1s = createMessageWithTimestamp(c, b1s, 2000); - - Message m21 = createMessageWithTimestamp(c, b2, 99); // latest incoming - Message m22 = createMessageWithTimestamp(c, b2, 1); - Message m23 = createMessageWithTimestamp(c, b2, 2); - - // Check! - assertEquals(m13.mId, Message.getLatestIncomingMessage(c, a1.mId).mId); - assertEquals(m21.mId, Message.getLatestIncomingMessage(c, a2.mId).mId); - - // No such account - assertEquals(null, Message.getLatestIncomingMessage(c, 9999999L)); - } - /** * Check if update on ACCOUNT_ID_ADD_TO_FIELD updates the cache properly. */