Merge "Use observers to manage new message notifications"

This commit is contained in:
Todd Kennedy 2011-05-10 17:09:07 -07:00 committed by Android (Google) Code Review
commit d31d64d330
9 changed files with 338 additions and 327 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

@ -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<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
@ -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<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.
*/