Use observers to manage new message notifications

We were sort of using observers to maintain the new message notifications.
However, other parts of the code would poke into the notification controller
to set things such as a list of newly added message IDs. Now, we rely
exclusively on db observers to manage notifications.

As a side effect of this, we now set the notification text correctly to be
the most recently _added_ message. This may be different than the most recently
sent message [since there may be a non-negligable delta between when the
message was sent and when it was received].

NOTE this still suffers from an outstanding bug where we continue to get
notifications when the Eamil UX is visible. That and monitoring changes to the
account table will be addressed in future CLs.

Change-Id: I4c68273716cc685574a1ca71e5d634f53fe0d882
This commit is contained in:
Todd Kennedy 2011-05-10 11:36:57 -07:00
parent a48abd7f95
commit 83693a6aca
9 changed files with 338 additions and 328 deletions

View File

@ -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 {

View File

@ -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) });
}
}
}

View File

@ -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;
@ -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<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 });
}
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<Long> 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<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;
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<Long> mMessageList = new HashSet<Long>();
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;
}
}
}

View File

@ -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);

View File

@ -1200,6 +1200,8 @@ public class MessageListFragment extends ListFragment
* <li>If {@code mailboxId} is not of a magic inbox (i.e. >= 0) and {@code
* accountId} is valid, reset the count of the specified account.
* </ul>
* 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) {

View File

@ -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);

View File

@ -55,7 +55,6 @@ 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.
@ -150,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);
}
/**
@ -212,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
@ -310,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();
@ -324,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<Long> messageIdList = new ArrayList<Long>();
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
@ -366,6 +341,7 @@ public class MailService extends Service {
@Override
public void onDestroy() {
super.onDestroy();
NotificationController.getInstance(mContext).watchForMessages(false);
Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback);
}
@ -716,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);
}
@ -746,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<Long> addedMessages) {
synchronized (mSyncReports) {
AccountSyncReport report = mSyncReports.get(accountId);
if (report == null || !report.notify) {
return;
}
}
NotificationController.getInstance(this)
.showNewMessageNotification(accountId, addedMessages);
}
/**
* @see ConnectivityManager#getBackgroundDataSetting()
*/

View File

@ -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);

View File

@ -1885,47 +1885,6 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
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.
*/