summaryrefslogtreecommitdiff
path: root/src/com/android/email/NotificationController.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/email/NotificationController.java')
-rw-r--r--src/com/android/email/NotificationController.java493
1 files changed, 304 insertions, 189 deletions
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<Long, MessageData> mNotificationMap;
@@ -166,15 +179,6 @@ public class NotificationController {
}
/**
- * 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.
*/
private int getNewMessageNotificationId(long accountId) {
@@ -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);
- }
- } else {
- MessageData data = mNotificationMap.remove(accountId);
- if (data == null) {
- // Not in map; nothing to do here
- return;
+ 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());
}
- // 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);
+ }
+ // Run this on the message notification handler
+ sNewMessageHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ ContentResolver resolver = mContext.getContentResolver();
+ HashMap<Long, long[]> table;
+ if (!watch) {
+ table = new HashMap<Long, long[]>();
+ for (Long key : mNotificationMap.keySet()) {
+ MessageData data = mNotificationMap.get(key);
+ table.put(key,
+ new long[] { data.mNotifiedMessageId, data.mNotifiedMessageCount });
}
- Uri uri = Account.RESET_NEW_MESSAGE_COUNT_URI;
- uri = ContentUris.withAppendedId(uri, accountId);
- resolver.update(uri, null, null, null);
+ 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
+ }
+ });
}
/**
- * 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.
+ * 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.
*/
- public void showNewMessageNotification(final long accountId,
- final ArrayList<Long> addedMessages) {
- if (addedMessages == null || addedMessages.size() == 0) {
- // No messages added; nothing to do here
- return;
- }
- MessageData data = mNotificationMap.get(accountId);
- if (data == null) {
+ 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.get(accountId);
+ if (data != null) return; // we're already observing; nothing to do
+
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);
}
- final HashSet<Long> 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;
+ }
+
+ /**
+ * 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.
+ */
+ 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);
- 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
+ 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;
- // 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.
- */
+ /** Information about the message(s) we're notifying the user about. */
private static class MessageData {
- final HashSet<Long> mMessageList = new HashSet<Long>();
+ /** The database observer */
ContentObserver mObserver;
+ /** Message ID used in the user notification */
+ long mNotifiedMessageId;
+ /** Message count used in the user notification */
+ int mNotifiedMessageCount;
+ }
+
+ /**
+ * Thread to handle all notification actions through its own {@link Looper}.
+ */
+ 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;
+ }
}
}