Remove notification if messages seen off device

If we receive new messages, we may display a notification to the user. If
those same messages are read elsewhere (i.e. via a web client), we will
remove the notification.

Change-Id: Iba09afe01942e0deaac8210fd6f9b315b1c8c93f
This commit is contained in:
Todd Kennedy 2011-05-03 14:42:26 -07:00
parent c96cd4a848
commit c4cdb11d24
23 changed files with 413 additions and 291 deletions

View File

@ -54,6 +54,7 @@ $(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/APPS/Email_intermedia
$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/com.android.emailcommon_intermediates) $(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/com.android.emailcommon_intermediates)
$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/com.android.emailcommon_intermediates) $(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/com.android.emailcommon_intermediates)
$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/com.android.emailcommon_intermediates) $(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/com.android.emailcommon_intermediates)
$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/com.android.emailcommon_intermediates)
# ************************************************ # ************************************************
# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST # NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST

View File

@ -97,6 +97,7 @@ public abstract class EmailContent {
public static final String FIELD_COLUMN_NAME = "field"; public static final String FIELD_COLUMN_NAME = "field";
public static final String ADD_COLUMN_NAME = "add"; public static final String ADD_COLUMN_NAME = "add";
public static final String SET_COLUMN_NAME = "set";
// Newly created objects get this id // Newly created objects get this id
public static final int NOT_SAVED = -1; public static final int NOT_SAVED = -1;

View File

@ -22,6 +22,8 @@ import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.os.RemoteException; import android.os.RemoteException;
import java.util.List;
public class AccountServiceProxy extends ServiceProxy implements IAccountService { public class AccountServiceProxy extends ServiceProxy implements IAccountService {
public static final String ACCOUNT_INTENT = "com.android.email.ACCOUNT_INTENT"; public static final String ACCOUNT_INTENT = "com.android.email.ACCOUNT_INTENT";
@ -44,7 +46,7 @@ public class AccountServiceProxy extends ServiceProxy implements IAccountService
} }
@Override @Override
public void notifyLoginFailed(final long accountId) throws RemoteException { public void notifyLoginFailed(final long accountId) {
setTask(new ProxyTask() { setTask(new ProxyTask() {
public void run() throws RemoteException { public void run() throws RemoteException {
mService.notifyLoginFailed(accountId); mService.notifyLoginFailed(accountId);
@ -53,7 +55,7 @@ public class AccountServiceProxy extends ServiceProxy implements IAccountService
} }
@Override @Override
public void notifyLoginSucceeded(final long accountId) throws RemoteException { public void notifyLoginSucceeded(final long accountId) {
setTask(new ProxyTask() { setTask(new ProxyTask() {
public void run() throws RemoteException { public void run() throws RemoteException {
mService.notifyLoginSucceeded(accountId); mService.notifyLoginSucceeded(accountId);
@ -62,16 +64,17 @@ public class AccountServiceProxy extends ServiceProxy implements IAccountService
} }
@Override @Override
public void notifyNewMessages(final long accountId) throws RemoteException { @SuppressWarnings("unchecked")
public void notifyNewMessages(final long accountId, final List messageIdList) {
setTask(new ProxyTask() { setTask(new ProxyTask() {
public void run() throws RemoteException { public void run() throws RemoteException {
mService.notifyNewMessages(accountId); mService.notifyNewMessages(accountId, messageIdList);
} }
}, "notifyNewMessages"); }, "notifyNewMessages");
} }
@Override @Override
public void accountDeleted() throws RemoteException { public void accountDeleted() {
setTask(new ProxyTask() { setTask(new ProxyTask() {
public void run() throws RemoteException { public void run() throws RemoteException {
mService.accountDeleted(); mService.accountDeleted();
@ -81,7 +84,7 @@ public class AccountServiceProxy extends ServiceProxy implements IAccountService
// The following call is synchronous, and should not be made from the UI thread // The following call is synchronous, and should not be made from the UI thread
@Override @Override
public void restoreAccountsIfNeeded() throws RemoteException { public void restoreAccountsIfNeeded() {
setTask(new ProxyTask() { setTask(new ProxyTask() {
public void run() throws RemoteException { public void run() throws RemoteException {
mService.restoreAccountsIfNeeded(); mService.restoreAccountsIfNeeded();
@ -92,7 +95,7 @@ public class AccountServiceProxy extends ServiceProxy implements IAccountService
// The following call is synchronous, and should not be made from the UI thread // The following call is synchronous, and should not be made from the UI thread
@Override @Override
public int getAccountColor(final long accountId) throws RemoteException { public int getAccountColor(final long accountId) {
setTask(new ProxyTask() { setTask(new ProxyTask() {
public void run() throws RemoteException{ public void run() throws RemoteException{
mReturn = mService.getAccountColor(accountId); mReturn = mService.getAccountColor(accountId);
@ -107,7 +110,7 @@ public class AccountServiceProxy extends ServiceProxy implements IAccountService
} }
// The following call is synchronous, and should not be made from the UI thread // The following call is synchronous, and should not be made from the UI thread
public Bundle getConfigurationData(final String accountType) throws RemoteException { public Bundle getConfigurationData(final String accountType) {
setTask(new ProxyTask() { setTask(new ProxyTask() {
public void run() throws RemoteException{ public void run() throws RemoteException{
mReturn = mService.getConfigurationData(accountType); mReturn = mService.getConfigurationData(accountType);
@ -122,7 +125,7 @@ public class AccountServiceProxy extends ServiceProxy implements IAccountService
} }
// The following call is synchronous, and should not be made from the UI thread // The following call is synchronous, and should not be made from the UI thread
public String getDeviceId() throws RemoteException { public String getDeviceId() {
setTask(new ProxyTask() { setTask(new ProxyTask() {
public void run() throws RemoteException{ public void run() throws RemoteException{
mReturn = mService.getDeviceId(); mReturn = mService.getDeviceId();

View File

@ -21,7 +21,7 @@ import android.os.Bundle;
interface IAccountService { interface IAccountService {
oneway void notifyLoginFailed(long accountId); oneway void notifyLoginFailed(long accountId);
oneway void notifyLoginSucceeded(long accountId); oneway void notifyLoginSucceeded(long accountId);
oneway void notifyNewMessages(long accountId); oneway void notifyNewMessages(long accountId, in List messageIdList);
void accountDeleted(); void accountDeleted();
void restoreAccountsIfNeeded(); void restoreAccountsIfNeeded();

View File

@ -58,6 +58,7 @@ import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.security.InvalidParameterException; import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -1142,7 +1143,7 @@ public class Controller {
* @param numNewMessages the number of new messages delivered * @param numNewMessages the number of new messages delivered
*/ */
public void updateMailboxCallback(MessagingException result, long accountId, public void updateMailboxCallback(MessagingException result, long accountId,
long mailboxId, int progress, int numNewMessages) { long mailboxId, int progress, int numNewMessages, ArrayList<Long> addedMessages) {
} }
/** /**
@ -1292,17 +1293,18 @@ public class Controller {
public void synchronizeMailboxStarted(long accountId, long mailboxId) { public void synchronizeMailboxStarted(long accountId, long mailboxId) {
synchronized (mListeners) { synchronized (mListeners) {
for (Result l : mListeners) { for (Result l : mListeners) {
l.updateMailboxCallback(null, accountId, mailboxId, 0, 0); l.updateMailboxCallback(null, accountId, mailboxId, 0, 0, null);
} }
} }
} }
@Override @Override
public void synchronizeMailboxFinished(long accountId, long mailboxId, public void synchronizeMailboxFinished(long accountId, long mailboxId,
int totalMessagesInMailbox, int numNewMessages) { int totalMessagesInMailbox, int numNewMessages, ArrayList<Long> addedMessages) {
synchronized (mListeners) { synchronized (mListeners) {
for (Result l : mListeners) { for (Result l : mListeners) {
l.updateMailboxCallback(null, accountId, mailboxId, 100, numNewMessages); l.updateMailboxCallback(null, accountId, mailboxId, 100, numNewMessages,
addedMessages);
} }
} }
} }
@ -1317,7 +1319,7 @@ public class Controller {
} }
synchronized (mListeners) { synchronized (mListeners) {
for (Result l : mListeners) { for (Result l : mListeners) {
l.updateMailboxCallback(me, accountId, mailboxId, 0, 0); l.updateMailboxCallback(me, accountId, mailboxId, 0, 0, null);
} }
} }
} }
@ -1566,7 +1568,7 @@ public class Controller {
long accountId = mbx.mAccountKey; long accountId = mbx.mAccountKey;
synchronized(mListeners) { synchronized(mListeners) {
for (Result listener : mListeners) { for (Result listener : mListeners) {
listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0); listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0, null);
} }
} }
} }

View File

@ -19,6 +19,8 @@ package com.android.email;
import com.android.email.Controller.Result; import com.android.email.Controller.Result;
import com.android.emailcommon.mail.MessagingException; import com.android.emailcommon.mail.MessagingException;
import java.util.ArrayList;
import android.os.Handler; import android.os.Handler;
/** /**
@ -106,12 +108,13 @@ public class ControllerResultUiThreadWrapper<T extends Result> extends Result {
@Override @Override
public void updateMailboxCallback(final MessagingException result, final long accountId, public void updateMailboxCallback(final MessagingException result, final long accountId,
final long mailboxId, final int progress, final int numNewMessages) { final long mailboxId, final int progress, final int numNewMessages,
final ArrayList<Long> addedMessages) {
run(new Runnable() { run(new Runnable() {
public void run() { public void run() {
if (!isRegistered()) return; if (!isRegistered()) return;
mWrappee.updateMailboxCallback(result, accountId, mailboxId, progress, mWrappee.updateMailboxCallback(result, accountId, mailboxId, progress,
numNewMessages); numNewMessages, addedMessages);
} }
}); });
} }

View File

@ -20,6 +20,7 @@ import com.android.emailcommon.mail.MessagingException;
import android.content.Context; import android.content.Context;
import java.util.ArrayList;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -80,10 +81,10 @@ public class GroupMessagingListener extends MessagingListener {
@Override @Override
synchronized public void synchronizeMailboxFinished(long accountId, long mailboxId, synchronized public void synchronizeMailboxFinished(long accountId, long mailboxId,
int totalMessagesInMailbox, int numNewMessages) { int totalMessagesInMailbox, int numNewMessages, ArrayList<Long> addedMessages) {
for (MessagingListener l : mListeners) { for (MessagingListener l : mListeners) {
l.synchronizeMailboxFinished(accountId, mailboxId, l.synchronizeMailboxFinished(accountId, mailboxId,
totalMessagesInMailbox, numNewMessages); totalMessagesInMailbox, numNewMessages, addedMessages);
} }
} }

View File

@ -18,7 +18,6 @@ package com.android.email;
import com.android.email.mail.Sender; import com.android.email.mail.Sender;
import com.android.email.mail.Store; import com.android.email.mail.Store;
import com.android.email.mail.StoreSynchronizer;
import com.android.emailcommon.Logging; import com.android.emailcommon.Logging;
import com.android.emailcommon.internet.MimeBodyPart; import com.android.emailcommon.internet.MimeBodyPart;
import com.android.emailcommon.internet.MimeHeader; import com.android.emailcommon.internet.MimeHeader;
@ -330,20 +329,19 @@ public class MessagingController implements Runnable {
mListeners.synchronizeMailboxStarted(account.mId, folder.mId); mListeners.synchronizeMailboxStarted(account.mId, folder.mId);
if ((folder.mFlags & Mailbox.FLAG_HOLDS_MAIL) == 0) { if ((folder.mFlags & Mailbox.FLAG_HOLDS_MAIL) == 0) {
// We don't hold messages, so, nothing to synchronize // We don't hold messages, so, nothing to synchronize
mListeners.synchronizeMailboxFinished(account.mId, folder.mId, 0, 0); mListeners.synchronizeMailboxFinished(account.mId, folder.mId, 0, 0, null);
return; return;
} }
NotificationController nc = NotificationController.getInstance(mContext); NotificationController nc = NotificationController.getInstance(mContext);
try { try {
processPendingActionsSynchronous(account); processPendingActionsSynchronous(account);
StoreSynchronizer.SyncResults results;
// Select generic sync or store-specific sync // Select generic sync or store-specific sync
results = synchronizeMailboxGeneric(account, folder); SyncResults results = synchronizeMailboxGeneric(account, folder);
mListeners.synchronizeMailboxFinished(account.mId, folder.mId, mListeners.synchronizeMailboxFinished(account.mId, folder.mId,
results.mTotalMessages, results.mTotalMessages,
results.mNewMessages); results.mAddedMessages.size(),
results.mAddedMessages);
// Clear authentication notification for this account // Clear authentication notification for this account
nc.cancelLoginFailedNotification(account.mId); nc.cancelLoginFailedNotification(account.mId);
} catch (MessagingException e) { } catch (MessagingException e) {
@ -409,17 +407,23 @@ public class MessagingController implements Runnable {
* @return results of the sync pass * @return results of the sync pass
* @throws MessagingException * @throws MessagingException
*/ */
private StoreSynchronizer.SyncResults synchronizeMailboxGeneric( private SyncResults synchronizeMailboxGeneric(
final EmailContent.Account account, final EmailContent.Mailbox folder) final EmailContent.Account account, final EmailContent.Mailbox folder)
throws MessagingException { throws MessagingException {
/*
* A list of IDs for messages that were downloaded and did not have the seen flag set.
* This serves as the "true" new message count reported to the user via notification.
*/
final ArrayList<Long> unseenMessages = new ArrayList<Long>();
Log.d(Logging.LOG_TAG, "*** synchronizeMailboxGeneric ***"); Log.d(Logging.LOG_TAG, "*** synchronizeMailboxGeneric ***");
ContentResolver resolver = mContext.getContentResolver(); ContentResolver resolver = mContext.getContentResolver();
// 0. We do not ever sync DRAFTS or OUTBOX (down or up) // 0. We do not ever sync DRAFTS or OUTBOX (down or up)
if (folder.mType == Mailbox.TYPE_DRAFTS || folder.mType == Mailbox.TYPE_OUTBOX) { if (folder.mType == Mailbox.TYPE_DRAFTS || folder.mType == Mailbox.TYPE_OUTBOX) {
int totalMessages = EmailContent.count(mContext, folder.getUri(), null, null); int totalMessages = EmailContent.count(mContext, folder.getUri(), null, null);
return new StoreSynchronizer.SyncResults(totalMessages, 0); return new SyncResults(totalMessages, unseenMessages);
} }
// 1. Get the message list from the local store and create an index of the uids // 1. Get the message list from the local store and create an index of the uids
@ -474,7 +478,7 @@ public class MessagingController implements Runnable {
|| folder.mType == Mailbox.TYPE_DRAFTS) { || folder.mType == Mailbox.TYPE_DRAFTS) {
if (!remoteFolder.exists()) { if (!remoteFolder.exists()) {
if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) {
return new StoreSynchronizer.SyncResults(0, 0); return new SyncResults(0, unseenMessages);
} }
} }
} }
@ -541,13 +545,6 @@ public class MessagingController implements Runnable {
} }
// 8. Download basic info about the new/unloaded messages (if any) // 8. Download basic info about the new/unloaded messages (if any)
/*
* A list of messages that were downloaded and which did not have the Seen flag set.
* This will serve to indicate the true "new" message count that will be reported to
* the user via notification.
*/
final ArrayList<Message> newMessages = new ArrayList<Message>();
/* /*
* Fetch the flags and envelope only of the new messages. This is intended to get us * Fetch the flags and envelope only of the new messages. This is intended to get us
* critical data as fast as possible, and then we'll fill in the details. * critical data as fast as possible, and then we'll fill in the details.
@ -584,7 +581,7 @@ public class MessagingController implements Runnable {
saveOrUpdate(localMessage, mContext); saveOrUpdate(localMessage, mContext);
// Track the "new" ness of the downloaded message // Track the "new" ness of the downloaded message
if (!message.isSet(Flag.SEEN)) { if (!message.isSet(Flag.SEEN)) {
newMessages.add(message); unseenMessages.add(localMessage.mId);
} }
} catch (MessagingException me) { } catch (MessagingException me) {
Log.e(Logging.LOG_TAG, Log.e(Logging.LOG_TAG,
@ -753,7 +750,7 @@ public class MessagingController implements Runnable {
// 14. Clean up and report results // 14. Clean up and report results
remoteFolder.close(false); remoteFolder.close(false);
return new StoreSynchronizer.SyncResults(remoteMessageCount, newMessages.size()); return new SyncResults(remoteMessageCount, unseenMessages);
} }
/** /**
@ -1960,4 +1957,20 @@ public class MessagingController implements Runnable {
return description; return description;
} }
} }
/** Results of the latest synchronization. */
private static class SyncResults {
/** The total # of messages in the folder */
public final int mTotalMessages;
/** A list of new message IDs; must not be {@code null} */
public final ArrayList<Long> mAddedMessages;
public SyncResults(int totalMessages, ArrayList<Long> addedMessages) {
if (addedMessages == null) {
throw new IllegalArgumentException("addedMessages must not be null");
}
mTotalMessages = totalMessages;
mAddedMessages = addedMessages;
}
}
} }

View File

@ -20,6 +20,8 @@ import com.android.emailcommon.mail.MessagingException;
import android.content.Context; import android.content.Context;
import java.util.ArrayList;
/** /**
* Defines the interface that MessagingController will use to callback to requesters. This class * Defines the interface that MessagingController will use to callback to requesters. This class
* is defined as non-abstract so that someone who wants to receive only a few messages can * is defined as non-abstract so that someone who wants to receive only a few messages can
@ -40,16 +42,25 @@ public class MessagingListener {
public void listFoldersFinished(long accountId) { public void listFoldersFinished(long accountId) {
} }
public void synchronizeMailboxStarted(long accountId, long mailboxId) public void synchronizeMailboxStarted(long accountId, long mailboxId) {
{
} }
public void synchronizeMailboxFinished(long accountId, /**
long mailboxId, int totalMessagesInMailbox, int numNewMessages) { * Synchronization of the mailbox finished. The mailbox and/or message databases have been
* updated accordingly.
*
* @param accountId The account that was synchronized
* @param mailboxId The mailbox that was synchronized
* @param totalMessagesInMailbox The total number of messages in the mailbox
* @param numNewMessages The number of new messages
* @param addedMessages Message IDs of messages that were added during the synchronization.
* These are new, unread messages. Messages that were previously read are not in this list.
*/
public void synchronizeMailboxFinished(long accountId, long mailboxId,
int totalMessagesInMailbox, int numNewMessages, ArrayList<Long> addedMessages) {
} }
public void synchronizeMailboxFailed(long accountId, long mailboxId, public void synchronizeMailboxFailed(long accountId, long mailboxId, Exception e) {
Exception e) {
} }
public void loadMessageForViewStarted(long messageId) { public void loadMessageForViewStarted(long messageId) {

View File

@ -23,6 +23,7 @@ import com.android.email.activity.setup.AccountSettingsXL;
import com.android.emailcommon.mail.Address; import com.android.emailcommon.mail.Address;
import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.Account; 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.Attachment;
import com.android.emailcommon.provider.EmailContent.Message; import com.android.emailcommon.provider.EmailContent.Message;
import com.android.emailcommon.utility.EmailAsyncTask; import com.android.emailcommon.utility.EmailAsyncTask;
@ -32,16 +33,26 @@ import com.google.common.annotations.VisibleForTesting;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.ContentObserver;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.media.AudioManager; import android.media.AudioManager;
import android.net.Uri; import android.net.Uri;
import android.os.Handler;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.style.TextAppearanceSpan; import android.text.style.TextAppearanceSpan;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.NoSuchElementException;
/** /**
* Class that manages notifications. * Class that manages notifications.
*/ */
@ -63,6 +74,12 @@ public class NotificationController {
private final AudioManager mAudioManager; private final AudioManager mAudioManager;
private final Bitmap mGenericSenderIcon; private final Bitmap mGenericSenderIcon;
private final Clock mClock; private final Clock mClock;
// TODO The service context used to create and manage the notification controller is NOT
// guaranteed to live forever. As such, we may lose the data in this structure. We should
// save / restore this data upon service termination / start. We'd also want to define
// the behaviour after a restart.
/** Maps account id to the message data */
private final HashMap<Long, MessageData> mNotificationMap;
/** Constructor */ /** Constructor */
@VisibleForTesting @VisibleForTesting
@ -74,6 +91,7 @@ public class NotificationController {
mGenericSenderIcon = BitmapFactory.decodeResource(mContext.getResources(), mGenericSenderIcon = BitmapFactory.decodeResource(mContext.getResources(),
R.drawable.ic_contact_picture); R.drawable.ic_contact_picture);
mClock = clock; mClock = clock;
mNotificationMap = new HashMap<Long, MessageData>();
} }
/** Singleton access */ /** Singleton access */
@ -97,11 +115,13 @@ public class NotificationController {
* @param largeIcon A large icon. May be {@code null} * @param largeIcon A large icon. May be {@code null}
* @param number A number to display using {@link Notification.Builder#setNumber(int)}. May * @param number A number to display using {@link Notification.Builder#setNumber(int)}. May
* be {@code null}. * be {@code null}.
* @param enableAudio If {@code false}, do not play any sound. Otherwise, play sound according
* to the settings for the given account.
* @return A {@link Notification} that can be sent to the notification service. * @return A {@link Notification} that can be sent to the notification service.
*/ */
private Notification createAccountNotification(Account account, String ticker, private Notification createAccountNotification(Account account, String ticker,
CharSequence title, String contentText, Intent intent, Bitmap largeIcon, CharSequence title, String contentText, Intent intent, Bitmap largeIcon,
Integer number) { Integer number, boolean enableAudio) {
// Pending Intent // Pending Intent
PendingIntent pending = null; PendingIntent pending = null;
if (intent != null) { if (intent != null) {
@ -119,7 +139,10 @@ public class NotificationController {
.setSmallIcon(R.drawable.stat_notify_email_generic) .setSmallIcon(R.drawable.stat_notify_email_generic)
.setWhen(mClock.getTime()) .setWhen(mClock.getTime())
.setTicker(ticker); .setTicker(ticker);
setupSoundAndVibration(builder, account);
if (enableAudio) {
setupSoundAndVibration(builder, account);
}
Notification notification = builder.getNotification(); Notification notification = builder.getNotification();
return notification; return notification;
@ -137,8 +160,8 @@ public class NotificationController {
*/ */
private void showAccountNotification(Account account, String ticker, String title, private void showAccountNotification(Account account, String ticker, String title,
String contentText, Intent intent, int notificationId) { String contentText, Intent intent, int notificationId) {
Notification notification = //nb.getNotification(); Notification notification = createAccountNotification(account, ticker, title, contentText,
createAccountNotification(account, ticker, title, contentText, intent, null, null); intent, null, null, true);
mNotificationManager.notify(notificationId, notification); mNotificationManager.notify(notificationId, notification);
} }
@ -165,16 +188,35 @@ public class NotificationController {
* @param accountId The ID of the account to cancel for. If {@code -1}, "new message" * @param accountId The ID of the account to cancel for. If {@code -1}, "new message"
* notifications for all accounts will be canceled. * notifications for all accounts will be canceled.
*/ */
public void cancelNewMessageNotification(long accountId) { public void cancelNewMessageNotification(final long accountId) {
if (accountId == -1) { if (accountId == -1) {
new Utility.ForEachAccount(mContext) { for (long id : mNotificationMap.keySet()) {
@Override cancelNewMessageNotification(id);
protected void performAction(long accountId) { }
cancelNewMessageNotification(accountId);
}
}.execute();
} else { } 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)); mNotificationManager.cancel(getNewMessageNotificationId(accountId));
// now do the database work
EmailAsyncTask.runAsyncParallel(new Runnable() {
@Override
public void run() {
ContentResolver resolver = mContext.getContentResolver();
if (myObserver != null) {
resolver.unregisterContentObserver(myObserver);
}
Uri uri = Account.RESET_NEW_MESSAGE_COUNT_URI;
uri = ContentUris.withAppendedId(uri, accountId);
resolver.update(uri, null, null, null);
}
});
} }
} }
@ -182,17 +224,58 @@ public class NotificationController {
* Show (or update) a "new message" notification for the given account. * Show (or update) a "new message" notification for the given account.
* *
* @param accountId The ID of the account to display a notification for. * @param accountId The ID of the account to display a notification for.
* @param unseenMessageCount The number of messages in the account that are unseen. * @param addedMessages A list of new message IDs added to the given account.
*/ */
public void showNewMessageNotification(final long accountId, final int unseenMessageCount, public void showNewMessageNotification(final long accountId,
final int justFetchedCount) { 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;
} else {
myObserver = data.mObserver;
}
EmailAsyncTask.runAsyncParallel(new Runnable() { EmailAsyncTask.runAsyncParallel(new Runnable() {
@Override @Override
public void run() { public void run() {
Notification n = createNewMessageNotification(accountId, unseenMessageCount); ContentResolver resolver = mContext.getContentResolver();
// Atomically update the unseen count
ContentValues cv = new ContentValues();
cv.put(EmailContent.FIELD_COLUMN_NAME, AccountColumns.NEW_MESSAGE_COUNT);
cv.put(EmailContent.ADD_COLUMN_NAME, addedMessages.size());
Uri uri = ContentUris.withAppendedId(Account.ADD_TO_FIELD_URI, accountId);
resolver.update(uri, cv, null, null);
// Get the unseen count
uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
int unseenMessageCount = Utility.getFirstRowInt(mContext, uri,
new String[] { AccountColumns.NEW_MESSAGE_COUNT }, null /*selection*/,
null /*selectionArgs*/, null /*sortOrder*/, 0 /*column*/, 0 /*default*/);
// Create the notification
Notification n = createNewMessageNotification(accountId, unseenMessageCount, true);
if (n == null) { if (n == null) {
return; return;
} }
// Register a content observer with one of the messages
uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId);
resolver.registerContentObserver(uri, false, myObserver);
// Make the notification visible
mNotificationManager.notify(getNewMessageNotificationId(accountId), n); mNotificationManager.notify(getNewMessageNotificationId(accountId), n);
} }
}); });
@ -222,7 +305,8 @@ public class NotificationController {
* NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
*/ */
@VisibleForTesting @VisibleForTesting
Notification createNewMessageNotification(long accountId, int unseenMessageCount) { Notification createNewMessageNotification(long accountId, int unseenMessageCount,
boolean enableAudio) {
final Account account = Account.restoreAccountWithId(mContext, accountId); final Account account = Account.restoreAccountWithId(mContext, accountId);
if (account == null) { if (account == null) {
return null; return null;
@ -244,8 +328,8 @@ public class NotificationController {
final Bitmap largeIcon = senderPhoto != null ? senderPhoto : mGenericSenderIcon; final Bitmap largeIcon = senderPhoto != null ? senderPhoto : mGenericSenderIcon;
final Integer number = unseenMessageCount > 1 ? unseenMessageCount : null; final Integer number = unseenMessageCount > 1 ? unseenMessageCount : null;
Notification notification = Notification notification = createAccountNotification(account, null, title, subject,
createAccountNotification(account, null, title, subject, intent, largeIcon, number); intent, largeIcon, number, enableAudio);
return notification; return notification;
} }
@ -416,4 +500,119 @@ public class NotificationController {
public void cancelSecurityNeededNotification() { public void cancelSecurityNeededNotification() {
cancelNotification(NOTIFICATION_ID_SECURITY_NEEDED); cancelNotification(NOTIFICATION_ID_SECURITY_NEEDED);
} }
/**
* Observer invoked whenever a message we're notifying the user about changes.
*/
private static class MessageContentObserver extends ContentObserver {
/** The account this observer is attached to */
private final long mAccountId;
/** A singular message ID to notify on */
private final long mMessageId;
/** The context */
private final Context mContext;
/** The handler we will be invoked on */
private final Handler mHandler;
MessageContentObserver(Handler handler, Context context, long accountId,
long messageId) {
super (handler);
mHandler = handler;
mContext = context;
mAccountId = accountId;
mMessageId = messageId;
}
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
final MessageData data = sInstance.mNotificationMap.get(mAccountId);
// If this account had been removed from the set of notifications or if the observer
// has been updated, make sure we don't get called again
if (data == null || data.mObserver != this) {
mContext.getContentResolver().unregisterContentObserver(this);
return;
}
// Ensure we're only handling one change at a time
EmailAsyncTask.runAsyncSerial(new Runnable() {
@Override
public void run() {
handleChange(data);
}
});
}
/**
* Performs any database operations to handle an observed change.
*
* NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
* @param data Message data for the observed account
*/
private void handleChange(MessageData data) {
Message message = Message.restoreMessageWithId(mContext, mMessageId);
if (message != null && !message.mFlagRead) {
// do nothing; wait until this message is modified
return;
}
// message removed or read; get another one in the list and update the notification
// Remove ourselves from the set of notifiers
ContentResolver resolver = mContext.getContentResolver();
resolver.unregisterContentObserver(this);
synchronized (data.mMessageList) {
data.mMessageList.remove(mMessageId);
}
try {
for (;;) {
long nextMessageId = data.mMessageList.iterator().next();
Message nextMessage = Message.restoreMessageWithId(mContext, nextMessageId);
if ((nextMessage == null) || (nextMessage.mFlagRead)) {
synchronized (data.mMessageList) {
data.mMessageList.remove(nextMessageId);
}
continue;
}
data.mObserver = new MessageContentObserver(mHandler, mContext, mAccountId,
nextMessageId);
Uri uri = ContentUris.withAppendedId(
EmailContent.Message.CONTENT_URI, nextMessageId);
resolver.registerContentObserver(uri, false, data.mObserver);
// Update the new message count
int unseenMessageCount = data.mMessageList.size();
ContentValues cv = new ContentValues();
cv.put(EmailContent.SET_COLUMN_NAME, unseenMessageCount);
uri = ContentUris.withAppendedId(
Account.RESET_NEW_MESSAGE_COUNT_URI, mAccountId);
resolver.update(uri, cv, null, null);
// Re-display the notification w/o audio
Notification n = sInstance.createNewMessageNotification(mAccountId,
unseenMessageCount, false);
sInstance.mNotificationManager.notify(
sInstance.getNewMessageNotificationId(mAccountId), n);
break;
}
} catch (NoSuchElementException e) {
// this is not an error; it means the list is empty, so, hide the notification
mHandler.post(new Runnable() {
@Override
public void run() {
// make sure we're on the UI thread to cancel the notification
sInstance.cancelNewMessageNotification(mAccountId);
}
});
}
}
}
/**
* Information about the message(s) we're notifying the user about.
*/
private static class MessageData {
final HashSet<Long> mMessageList = new HashSet<Long>();
ContentObserver mObserver;
}
} }

View File

@ -385,7 +385,8 @@ public class RefreshManager {
*/ */
@Override @Override
public void updateMailboxCallback(MessagingException exception, long accountId, public void updateMailboxCallback(MessagingException exception, long accountId,
long mailboxId, int progress, int dontUseNumNewMessages) { long mailboxId, int progress, int dontUseNumNewMessages,
ArrayList<Long> addedMessages) {
if (LOG_ENABLED) { if (LOG_ENABLED) {
Log.d(Logging.LOG_TAG, "updateMailboxCallback " + accountId + ", " Log.d(Logging.LOG_TAG, "updateMailboxCallback " + accountId + ", "
+ mailboxId + ", " + progress + ", " + exceptionToString(exception)); + mailboxId + ", " + progress + ", " + exceptionToString(exception));

View File

@ -28,6 +28,8 @@ import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.Account; import com.android.emailcommon.provider.EmailContent.Account;
import com.android.emailcommon.provider.EmailContent.Mailbox; import com.android.emailcommon.provider.EmailContent.Mailbox;
import java.util.ArrayList;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.Dialog; import android.app.Dialog;
@ -286,7 +288,8 @@ public class AccountFolderList extends Activity implements AccountFolderListFrag
@Override @Override
public void updateMailboxCallback(MessagingException result, long accountKey, public void updateMailboxCallback(MessagingException result, long accountKey,
long mailboxKey, int progress, int numNewMessages) { long mailboxKey, int progress, int numNewMessages,
ArrayList<Long> addedMessages) {
updateProgress(result, progress); updateProgress(result, progress);
} }

View File

@ -28,6 +28,8 @@ import com.android.emailcommon.provider.EmailContent.Message;
import com.android.emailcommon.provider.EmailContent.MessageColumns; import com.android.emailcommon.provider.EmailContent.MessageColumns;
import com.android.emailcommon.utility.Utility; import com.android.emailcommon.utility.Utility;
import java.util.ArrayList;
import android.app.Activity; import android.app.Activity;
import android.app.ListFragment; import android.app.ListFragment;
import android.content.Context; import android.content.Context;
@ -448,7 +450,8 @@ public class AccountFolderListFragment extends ListFragment
private class ControllerResults extends Controller.Result { private class ControllerResults extends Controller.Result {
@Override @Override
public void updateMailboxCallback(MessagingException result, long accountKey, public void updateMailboxCallback(MessagingException result, long accountKey,
long mailboxKey, int progress, int numNewMessages) { long mailboxKey, int progress, int numNewMessages,
ArrayList<Long> addedMessages) {
if (progress == 100) { if (progress == 100) {
updateAccounts(); updateAccounts();
} }

View File

@ -20,6 +20,7 @@ import com.android.email.Controller;
import com.android.email.ControllerResultUiThreadWrapper; import com.android.email.ControllerResultUiThreadWrapper;
import com.android.email.Email; import com.android.email.Email;
import com.android.email.MessagingExceptionStrings; import com.android.email.MessagingExceptionStrings;
import com.android.email.NotificationController;
import com.android.email.R; import com.android.email.R;
import com.android.emailcommon.Logging; import com.android.emailcommon.Logging;
import com.android.emailcommon.mail.MessagingException; import com.android.emailcommon.mail.MessagingException;
@ -51,6 +52,7 @@ import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import java.security.InvalidParameterException; import java.security.InvalidParameterException;
import java.util.ArrayList;
/** /**
* The main Email activity, which is used on both the tablet and the phone. * The main Email activity, which is used on both the tablet and the phone.
@ -480,7 +482,7 @@ public class EmailActivity extends Activity implements View.OnClickListener {
@Override @Override
public void updateMailboxCallback(MessagingException result, long accountId, long mailboxId, public void updateMailboxCallback(MessagingException result, long accountId, long mailboxId,
int progress, int numNewMessages) { int progress, int numNewMessages, ArrayList<Long> addedMessages) {
handleError(result, accountId, progress); handleError(result, accountId, progress);
} }

View File

@ -28,6 +28,8 @@ import com.android.emailcommon.provider.EmailContent.Account;
import com.android.emailcommon.provider.EmailContent.AccountColumns; import com.android.emailcommon.provider.EmailContent.AccountColumns;
import com.android.emailcommon.utility.Utility; import com.android.emailcommon.utility.Utility;
import java.util.ArrayList;
import android.app.ActionBar; import android.app.ActionBar;
import android.app.Activity; import android.app.Activity;
import android.content.ContentUris; import android.content.ContentUris;
@ -288,7 +290,7 @@ public class MailboxList extends Activity implements MailboxListFragment.Callbac
@Override @Override
public void updateMailboxCallback(MessagingException result, long accountKey, public void updateMailboxCallback(MessagingException result, long accountKey,
long mailboxKey, int progress, int numNewMessages) { long mailboxKey, int progress, int numNewMessages, ArrayList<Long> addedMessages) {
if (accountKey == mAccountId) { if (accountKey == mAccountId) {
updateBanner(result, progress); updateBanner(result, progress);
updateProgress(result, progress); updateProgress(result, progress);

View File

@ -242,14 +242,6 @@ public abstract class Store {
return com.android.email.activity.setup.AccountSetupIncoming.class; return com.android.email.activity.setup.AccountSetupIncoming.class;
} }
/**
* Get class of sync'er for this Store class
* @return Message Sync controller, or null to use default
*/
public StoreSynchronizer getMessageSynchronizer() {
return null;
}
/** /**
* Some stores cannot download a message based only on the uid, and need the message structure * Some stores cannot download a message based only on the uid, and need the message structure
* to be preloaded and provided to them. This method allows a remote store to signal this * to be preloaded and provided to them. This method allows a remote store to signal this

View File

@ -1,77 +0,0 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.email.mail;
import com.android.email.MessagingListener;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.provider.EmailContent;
import com.android.email.GroupMessagingListener;
import android.content.Context;
/**
* This interface allows a store to define a completely different synchronizer algorithm,
* as necessary.
*/
public interface StoreSynchronizer {
/**
* An object of this class is returned by SynchronizeMessagesSynchronous to report
* the results of the sync run.
*/
public static class SyncResults {
/**
* The total # of messages in the folder
*/
public int mTotalMessages;
/**
* The # of new messages in the folder
*/
public int mNewMessages;
public SyncResults(int totalMessages, int newMessages) {
mTotalMessages = totalMessages;
mNewMessages = newMessages;
}
}
/**
* The job of this method is to synchronize messages between a remote folder and the
* corresponding local folder.
*
* The following callbacks should be called during this operation:
* {@link MessagingListener#synchronizeMailboxNewMessage(Account, String, Message)}
* {@link MessagingListener#synchronizeMailboxRemovedMessage(Account, String, Message)}
*
* Callbacks (through listeners) *must* be synchronized on the listeners object, e.g.
* synchronized (listeners) {
* for(MessagingListener listener : listeners) {
* listener.synchronizeMailboxNewMessage(account, folder, message);
* }
* }
*
* @param account The account to synchronize
* @param folder The folder to synchronize
* @param listeners callbacks to make during sync operation
* @param context if needed for making system calls
* @return an object describing the sync results
*/
public SyncResults SynchronizeMessagesSynchronous(
EmailContent.Account account, EmailContent.Mailbox folder,
GroupMessagingListener listeners, Context context) throws MessagingException;
}

View File

@ -18,7 +18,6 @@ package com.android.email.mail.store;
import com.android.email.ExchangeUtils; import com.android.email.ExchangeUtils;
import com.android.email.mail.Store; import com.android.email.mail.Store;
import com.android.email.mail.StoreSynchronizer;
import com.android.emailcommon.mail.Folder; import com.android.emailcommon.mail.Folder;
import com.android.emailcommon.mail.MessagingException; import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.provider.EmailContent.Account; import com.android.emailcommon.provider.EmailContent.Account;
@ -83,18 +82,6 @@ public class ExchangeStore extends Store {
return com.android.email.activity.setup.AccountSetupExchange.class; return com.android.email.activity.setup.AccountSetupExchange.class;
} }
/**
* Get class of sync'er for this Store class. Because exchange Sync rules are so different
* than IMAP or POP3, it's likely that an Exchange implementation will need its own sync
* controller. If so, this function must return a non-null value.
*
* @return Message Sync controller, or null to use default
*/
@Override
public StoreSynchronizer getMessageSynchronizer() {
return null;
}
/** /**
* Inform MessagingController that this store requires message structures to be prefetched * Inform MessagingController that this store requires message structures to be prefetched
* before it can fetch message bodies (this is due to EAS protocol restrictions.) * before it can fetch message bodies (this is due to EAS protocol restrictions.)

View File

@ -1664,8 +1664,16 @@ public class EmailProvider extends ContentProvider {
if (cache != null) { if (cache != null) {
cache.lock(id); cache.lock(id);
} }
ContentValues newMessageCount = CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT;
if (values != null) {
Long set = values.getAsLong(EmailContent.SET_COLUMN_NAME);
if (set != null) {
newMessageCount = new ContentValues();
newMessageCount.put(Account.NEW_MESSAGE_COUNT, set);
}
}
try { try {
result = db.update(tableName, CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT, result = db.update(tableName, newMessageCount,
whereWithId(id, selection), selectionArgs); whereWithId(id, selection), selectionArgs);
} finally { } finally {
if (cache != null) { if (cache != null) {

View File

@ -25,16 +25,16 @@ import com.android.email.VendorPolicyLoader;
import com.android.emailcommon.Configuration; import com.android.emailcommon.Configuration;
import com.android.emailcommon.Device; import com.android.emailcommon.Device;
import com.android.emailcommon.service.IAccountService; import com.android.emailcommon.service.IAccountService;
import com.android.emailcommon.utility.Utility; import com.android.emailcommon.utility.EmailAsyncTask;
import android.app.Service; import android.app.Service;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.os.RemoteException;
import java.io.IOException; import java.io.IOException;
import java.util.List;
public class AccountService extends Service { public class AccountService extends Service {
@ -44,37 +44,38 @@ public class AccountService extends Service {
private final IAccountService.Stub mBinder = new IAccountService.Stub() { private final IAccountService.Stub mBinder = new IAccountService.Stub() {
@Override @Override
public void notifyLoginFailed(long accountId) throws RemoteException { public void notifyLoginFailed(long accountId) {
NotificationController.getInstance(mContext).showLoginFailedNotification(accountId); NotificationController.getInstance(mContext).showLoginFailedNotification(accountId);
} }
@Override @Override
public void notifyLoginSucceeded(long accountId) throws RemoteException { public void notifyLoginSucceeded(long accountId) {
NotificationController.getInstance(mContext).cancelLoginFailedNotification(accountId); NotificationController.getInstance(mContext).cancelLoginFailedNotification(accountId);
} }
@Override @Override
public void notifyNewMessages(long accountId) throws RemoteException { @SuppressWarnings("unchecked")
MailService.actionNotifyNewMessages(mContext, accountId); public void notifyNewMessages(long accountId, List messageIdList) {
MailService.actionNotifyNewMessages(mContext, accountId, messageIdList);
} }
@Override @Override
public void restoreAccountsIfNeeded() throws RemoteException { public void restoreAccountsIfNeeded() {
AccountBackupRestore.restoreAccountsIfNeeded(mContext); AccountBackupRestore.restoreAccountsIfNeeded(mContext);
} }
@Override @Override
public void accountDeleted() throws RemoteException { public void accountDeleted() {
MailService.accountDeleted(mContext); MailService.accountDeleted(mContext);
} }
@Override @Override
public int getAccountColor(long accountId) throws RemoteException { public int getAccountColor(long accountId) {
return ResourceHelper.getInstance(mContext).getAccountColor(accountId); return ResourceHelper.getInstance(mContext).getAccountColor(accountId);
} }
@Override @Override
public Bundle getConfigurationData(String accountType) throws RemoteException { public Bundle getConfigurationData(String accountType) {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putBoolean(Configuration.EXCHANGE_CONFIGURATION_USE_ALTERNATE_STRINGS, bundle.putBoolean(Configuration.EXCHANGE_CONFIGURATION_USE_ALTERNATE_STRINGS,
VendorPolicyLoader.getInstance(mContext).useAlternateExchangeStrings()); VendorPolicyLoader.getInstance(mContext).useAlternateExchangeStrings());
@ -82,9 +83,9 @@ public class AccountService extends Service {
} }
@Override @Override
public String getDeviceId() throws RemoteException { public String getDeviceId() {
try { try {
Utility.runAsync(new Runnable() { EmailAsyncTask.runAsyncSerial(new Runnable() {
@Override @Override
public void run() { public void run() {
// Make sure the service is properly running (re: lifecycle) // Make sure the service is properly running (re: lifecycle)

View File

@ -27,11 +27,11 @@ import com.android.emailcommon.AccountManagerTypes;
import com.android.emailcommon.mail.MessagingException; import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.Account; import com.android.emailcommon.provider.EmailContent.Account;
import com.android.emailcommon.provider.EmailContent.AccountColumns;
import com.android.emailcommon.provider.EmailContent.HostAuth; import com.android.emailcommon.provider.EmailContent.HostAuth;
import com.android.emailcommon.provider.EmailContent.Mailbox; import com.android.emailcommon.provider.EmailContent.Mailbox;
import com.android.emailcommon.utility.AccountReconciler; import com.android.emailcommon.utility.AccountReconciler;
import com.android.emailcommon.utility.Utility; import com.android.emailcommon.utility.EmailAsyncTask;
import com.google.common.annotations.VisibleForTesting;
import android.accounts.AccountManager; import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback; import android.accounts.AccountManagerCallback;
@ -47,7 +47,6 @@ import android.database.Cursor;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder; import android.os.IBinder;
import android.os.SystemClock; import android.os.SystemClock;
import android.text.TextUtils; import android.text.TextUtils;
@ -56,6 +55,7 @@ import android.util.Log;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.logging.Logger;
/** /**
* Background service for refreshing non-push email accounts. * Background service for refreshing non-push email accounts.
@ -82,24 +82,25 @@ public class MailService extends Service {
private static final String EXTRA_ACCOUNT = "com.android.email.intent.extra.ACCOUNT"; private static final String EXTRA_ACCOUNT = "com.android.email.intent.extra.ACCOUNT";
private static final String EXTRA_ACCOUNT_INFO = "com.android.email.intent.extra.ACCOUNT_INFO"; private static final String EXTRA_ACCOUNT_INFO = "com.android.email.intent.extra.ACCOUNT_INFO";
private static final String EXTRA_DEBUG_WATCHDOG = "com.android.email.intent.extra.WATCHDOG"; private static final String EXTRA_DEBUG_WATCHDOG = "com.android.email.intent.extra.WATCHDOG";
private static final String EXTRA_MESSAGE_ID_COUNT =
"com.android.email.intent.extra.MESSAGE_ID_COUNT";
private static final String EXTRA_MESSAGE_ID_PREFIX =
"com.android.email.intent.extra.MESSAGE_ID_";
private static final int WATCHDOG_DELAY = 10 * 60 * 1000; // 10 minutes /** Time between watchdog checks; in milliseconds */
private static final long WATCHDOG_DELAY = 10 * 60 * 1000; // 10 minutes
// Sentinel value asking to update mSyncReports if it's currently empty // Sentinel value asking to update mSyncReports if it's currently empty
/*package*/ static final int SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY = -1; /*package*/ static final int SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY = -1;
// Sentinel value asking that mSyncReports be rebuilt // Sentinel value asking that mSyncReports be rebuilt
/*package*/ static final int SYNC_REPORTS_RESET = -2; /*package*/ static final int SYNC_REPORTS_RESET = -2;
private static final String[] NEW_MESSAGE_COUNT_PROJECTION =
new String[] {AccountColumns.NEW_MESSAGE_COUNT};
private static MailService sMailService; private static MailService sMailService;
/*package*/ Controller mController; /*package*/ Controller mController;
private final Controller.Result mControllerCallback = new ControllerResults(); private final Controller.Result mControllerCallback = new ControllerResults();
private ContentResolver mContentResolver; private ContentResolver mContentResolver;
private Context mContext; private Context mContext;
private Handler mHandler = new Handler();
private int mStartId; private int mStartId;
@ -150,28 +151,7 @@ public class MailService extends Service {
* @param accountId account to clear, or -1 for all accounts * @param accountId account to clear, or -1 for all accounts
*/ */
public static void resetNewMessageCount(final Context context, final long accountId) { public static void resetNewMessageCount(final Context context, final long accountId) {
synchronized (mSyncReports) {
for (AccountSyncReport report : mSyncReports.values()) {
if (accountId == -1 || accountId == report.accountId) {
report.unseenMessageCount = 0;
report.lastUnseenMessageCount = 0;
}
}
}
// Clear notification
NotificationController.getInstance(context).cancelNewMessageNotification(accountId); NotificationController.getInstance(context).cancelNewMessageNotification(accountId);
// now do the database - all accounts, or just one of them
Utility.runAsync(new Runnable() {
@Override
public void run() {
Uri uri = Account.RESET_NEW_MESSAGE_COUNT_URI;
if (accountId != -1) {
uri = ContentUris.withAppendedId(uri, accountId);
}
context.getContentResolver().update(uri, null, null, null);
}
});
} }
/** /**
@ -182,10 +162,21 @@ public class MailService extends Service {
* @param context a context * @param context a context
* @param accountId the id of the account that is reporting new messages * @param accountId the id of the account that is reporting new messages
*/ */
public static void actionNotifyNewMessages(Context context, long accountId) { @SuppressWarnings("unchecked")
public static void actionNotifyNewMessages(
Context context, long accountId, List messageIdList) {
Intent i = new Intent(ACTION_NOTIFY_MAIL); Intent i = new Intent(ACTION_NOTIFY_MAIL);
i.setClass(context, MailService.class); i.setClass(context, MailService.class);
i.putExtra(EXTRA_ACCOUNT, accountId); i.putExtra(EXTRA_ACCOUNT, accountId);
int listSize = 0;
if (messageIdList != null) {
listSize = messageIdList.size();
for (int j = 0; j < listSize; j++) {
long messageId = (Long) messageIdList.get(j);
i.putExtra(EXTRA_MESSAGE_ID_PREFIX + j, messageId);
}
}
i.putExtra(EXTRA_MESSAGE_ID_COUNT, listSize);
context.startService(i); context.startService(i);
} }
@ -203,7 +194,7 @@ public class MailService extends Service {
// Restore accounts, if it has not happened already // Restore accounts, if it has not happened already
AccountBackupRestore.restoreAccountsIfNeeded(this); AccountBackupRestore.restoreAccountsIfNeeded(this);
Utility.runAsync(new Runnable() { EmailAsyncTask.runAsyncParallel(new Runnable() {
@Override @Override
public void run() { public void run() {
reconcilePopImapAccountsSync(MailService.this); reconcilePopImapAccountsSync(MailService.this);
@ -224,7 +215,7 @@ public class MailService extends Service {
if (ACTION_CHECK_MAIL.equals(action)) { if (ACTION_CHECK_MAIL.equals(action)) {
// DB access required to satisfy this intent, so offload from UI thread // DB access required to satisfy this intent, so offload from UI thread
Utility.runAsync(new Runnable() { EmailAsyncTask.runAsyncParallel(new Runnable() {
@Override @Override
public void run() { public void run() {
// If we have the data, restore the last-sync-times for each account // If we have the data, restore the last-sync-times for each account
@ -280,7 +271,7 @@ public class MailService extends Service {
if (Email.DEBUG) { if (Email.DEBUG) {
Log.d(LOG_TAG, "action: delete exchange accounts"); Log.d(LOG_TAG, "action: delete exchange accounts");
} }
Utility.runAsync(new Runnable() { EmailAsyncTask.runAsyncParallel(new Runnable() {
public void run() { public void run() {
Cursor c = mContentResolver.query(Account.CONTENT_URI, Account.ID_PROJECTION, Cursor c = mContentResolver.query(Account.CONTENT_URI, Account.ID_PROJECTION,
null, null, null); null, null, null);
@ -304,7 +295,7 @@ public class MailService extends Service {
if (Email.DEBUG) { if (Email.DEBUG) {
Log.d(LOG_TAG, "action: send pending mail"); Log.d(LOG_TAG, "action: send pending mail");
} }
Utility.runAsync(new Runnable() { EmailAsyncTask.runAsyncParallel(new Runnable() {
public void run() { public void run() {
mController.sendPendingMessages(accountId); mController.sendPendingMessages(accountId);
} }
@ -315,17 +306,14 @@ public class MailService extends Service {
if (Email.DEBUG) { if (Email.DEBUG) {
Log.d(LOG_TAG, "action: reschedule"); Log.d(LOG_TAG, "action: reschedule");
} }
final NotificationController nc = NotificationController.getInstance(this);
// DB access required to satisfy this intent, so offload from UI thread // DB access required to satisfy this intent, so offload from UI thread
Utility.runAsync(new Runnable() { EmailAsyncTask.runAsyncParallel(new Runnable() {
@Override @Override
public void run() { public void run() {
// Clear all notifications, in case account list has changed. // Clear all notifications, in case account list has changed.
// NotificationController
// TODO Clear notifications for non-existing accounts. Now that we have .getInstance(MailService.this)
// separate notifications for each account, NotificationController should be .cancelNewMessageNotification(-1);
// able to do that.
nc.cancelNewMessageNotification(-1);
// When called externally, we refresh the sync reports table to pick up // When called externally, we refresh the sync reports table to pick up
// any changes in the account list or account settings // any changes in the account list or account settings
@ -337,23 +325,23 @@ public class MailService extends Service {
}); });
} else if (ACTION_NOTIFY_MAIL.equals(action)) { } else if (ACTION_NOTIFY_MAIL.equals(action)) {
// DB access required to satisfy this intent, so offload from UI thread // DB access required to satisfy this intent, so offload from UI thread
Utility.runAsync(new Runnable() { EmailAsyncTask.runAsyncParallel(new Runnable() {
@Override @Override
public void run() { public void run() {
// Get the current new message count int newMessageCount = intent.getIntExtra(EXTRA_MESSAGE_ID_COUNT, 0);
Cursor c = mContentResolver.query( ArrayList<Long> messageIdList = new ArrayList<Long>();
ContentUris.withAppendedId(Account.CONTENT_URI, accountId), for (int i = 0; i < newMessageCount; i++) {
NEW_MESSAGE_COUNT_PROJECTION, null, null, null); final long messageId =
int newMessageCount = 0; intent.getLongExtra(EXTRA_MESSAGE_ID_PREFIX + i, -1L);
try { if (messageId <= 0) {
if (c.moveToFirst()) { // What else to do here?? This should never happen ...
newMessageCount = c.getInt(0); Log.w(LOG_TAG, "invalid message id in notification; id: " + messageId);
updateAccountReport(accountId, newMessageCount); continue;
notifyNewMessages(accountId);
} }
} finally { messageIdList.add(messageId);
c.close();
} }
updateAccountReport(accountId, newMessageCount);
notifyNewMessages(accountId, messageIdList);
if (Email.DEBUG) { if (Email.DEBUG) {
Log.d(LOG_TAG, "notify accountId=" + Long.toString(accountId) Log.d(LOG_TAG, "notify accountId=" + Long.toString(accountId)
+ " count=" + newMessageCount); + " count=" + newMessageCount);
@ -404,10 +392,7 @@ public class MailService extends Service {
AccountSyncReport oldReport = oldSyncReports.get(newReport.accountId); AccountSyncReport oldReport = oldSyncReports.get(newReport.accountId);
if (oldReport != null) { if (oldReport != null) {
newReport.prevSyncTime = oldReport.prevSyncTime; newReport.prevSyncTime = oldReport.prevSyncTime;
if (newReport.syncInterval > 0 && newReport.prevSyncTime != 0) { newReport.setNextSyncTime();
newReport.nextSyncTime =
newReport.prevSyncTime + (newReport.syncInterval * 1000 * 60);
}
} }
} }
} }
@ -533,38 +518,32 @@ public class MailService extends Service {
* TODO: Look more closely at syncEnabled and see if we can simply coalesce it into * TODO: Look more closely at syncEnabled and see if we can simply coalesce it into
* syncInterval (e.g. if !syncEnabled, set syncInterval to -1). * syncInterval (e.g. if !syncEnabled, set syncInterval to -1).
*/ */
/*package*/ static class AccountSyncReport { @VisibleForTesting
static class AccountSyncReport {
long accountId; long accountId;
long prevSyncTime; // 0 == unknown /** The time of the last sync, or, {@code 0}, the last sync time is unknown. */
long nextSyncTime; // 0 == ASAP -1 == don't sync long prevSyncTime;
/** The time of the next sync. If {@code 0}, sync ASAP. If {@code 1}, don't sync. */
/** # of "unseen" messages to show in notification */ long nextSyncTime;
int unseenMessageCount; /** Minimum time between syncs; in minutes. */
int syncInterval;
/** If {@code true}, show system notifications. */
boolean notify;
/** If {@code true}, auto sync is enabled. */
boolean syncEnabled;
/** /**
* # of unseen, the value shown on the last notification. Used to * Sets the next sync time using the previous sync time and sync interval.
* calculate "the number of messages that have just been fetched".
*
* TODO It's a sort of cheating. Should we use the "real" number? The only difference
* is the first notification after reboot / process restart.
*/ */
int lastUnseenMessageCount; void setNextSyncTime() {
if (syncInterval > 0 && prevSyncTime != 0) {
int syncInterval; nextSyncTime = prevSyncTime + (syncInterval * 1000 * 60);
boolean notify; }
boolean syncEnabled; // whether auto sync is enabled for this account
/** # of messages that have just been fetched */
int getJustFetchedMessageCount() {
return unseenMessageCount - lastUnseenMessageCount;
} }
@Override @Override
public String toString() { public String toString() {
return "id=" + accountId return "id=" + accountId + " prevSync=" + prevSyncTime + " nextSync=" + nextSyncTime;
+ " prevSync=" + prevSyncTime + " nextSync=" + nextSyncTime + " numUnseen="
+ unseenMessageCount;
} }
} }
@ -644,8 +623,6 @@ public class MailService extends Service {
report.accountId = account.mId; report.accountId = account.mId;
report.prevSyncTime = 0; report.prevSyncTime = 0;
report.nextSyncTime = (syncInterval > 0) ? 0 : -1; // 0 == ASAP -1 == no sync report.nextSyncTime = (syncInterval > 0) ? 0 : -1; // 0 == ASAP -1 == no sync
report.unseenMessageCount = 0;
report.lastUnseenMessageCount = 0;
report.syncInterval = syncInterval; report.syncInterval = syncInterval;
report.notify = (account.mFlags & Account.FLAGS_NOTIFY_NEW_MAIL) != 0; report.notify = (account.mFlags & Account.FLAGS_NOTIFY_NEW_MAIL) != 0;
@ -685,12 +662,7 @@ public class MailService extends Service {
// report found - update it (note - editing the report while in-place in the hashmap) // report found - update it (note - editing the report while in-place in the hashmap)
report.prevSyncTime = SystemClock.elapsedRealtime(); report.prevSyncTime = SystemClock.elapsedRealtime();
if (report.syncInterval > 0) { report.setNextSyncTime();
report.nextSyncTime = report.prevSyncTime + (report.syncInterval * 1000 * 60);
}
if (newCount != -1) {
report.unseenMessageCount = newCount;
}
if (Email.DEBUG) { if (Email.DEBUG) {
Log.d(LOG_TAG, "update account " + report.toString()); Log.d(LOG_TAG, "update account " + report.toString());
} }
@ -723,10 +695,7 @@ public class MailService extends Service {
if (report != null) { if (report != null) {
if (report.prevSyncTime == 0) { if (report.prevSyncTime == 0) {
report.prevSyncTime = prevSync; report.prevSyncTime = prevSync;
if (report.syncInterval > 0 && report.prevSyncTime != 0) { report.setNextSyncTime();
report.nextSyncTime =
report.prevSyncTime + (report.syncInterval * 1000 * 60);
}
} }
} }
} }
@ -736,7 +705,8 @@ public class MailService extends Service {
class ControllerResults extends Controller.Result { class ControllerResults extends Controller.Result {
@Override @Override
public void updateMailboxCallback(MessagingException result, long accountId, public void updateMailboxCallback(MessagingException result, long accountId,
long mailboxId, int progress, int numNewMessages) { long mailboxId, int progress, int numNewMessages,
ArrayList<Long> addedMessages) {
// First, look for authentication failures and notify // First, look for authentication failures and notify
//checkAuthenticationStatus(result, accountId); //checkAuthenticationStatus(result, accountId);
if (result != null || progress == 100) { if (result != null || progress == 100) {
@ -747,7 +717,7 @@ public class MailService extends Service {
if (progress == 100) { if (progress == 100) {
updateAccountReport(accountId, numNewMessages); updateAccountReport(accountId, numNewMessages);
if (numNewMessages > 0) { if (numNewMessages > 0) {
notifyNewMessages(accountId); notifyNewMessages(accountId, addedMessages);
} }
} else { } else {
updateAccountReport(accountId, -1); updateAccountReport(accountId, -1);
@ -779,21 +749,16 @@ public class MailService extends Service {
/** /**
* Show "new message" notification for an account. (Notification is shown per account.) * Show "new message" notification for an account. (Notification is shown per account.)
*/ */
private void notifyNewMessages(final long accountId) { private void notifyNewMessages(final long accountId, ArrayList<Long> addedMessages) {
final int unseenMessageCount;
final int justFetchedCount;
synchronized (mSyncReports) { synchronized (mSyncReports) {
AccountSyncReport report = mSyncReports.get(accountId); AccountSyncReport report = mSyncReports.get(accountId);
if (report == null || report.unseenMessageCount == 0 || !report.notify) { if (report == null || !report.notify) {
return; return;
} }
unseenMessageCount = report.unseenMessageCount;
justFetchedCount = report.getJustFetchedMessageCount();
report.lastUnseenMessageCount = report.unseenMessageCount;
} }
NotificationController.getInstance(this).showNewMessageNotification(accountId, NotificationController.getInstance(this)
unseenMessageCount, justFetchedCount); .showNewMessageNotification(accountId, addedMessages);
} }
/** /**
@ -874,7 +839,8 @@ public class MailService extends Service {
* @param blockExternalChanges FOR TESTING ONLY - block backups, security changes, etc. * @param blockExternalChanges FOR TESTING ONLY - block backups, security changes, etc.
* @param resolver the content resolver for making provider updates (injected for testability) * @param resolver the content resolver for making provider updates (injected for testability)
*/ */
/* package */ public static void reconcileAccountsWithAccountManager(Context context, @VisibleForTesting
public static void reconcileAccountsWithAccountManager(Context context,
List<Account> emailProviderAccounts, android.accounts.Account[] accountManagerAccounts, List<Account> emailProviderAccounts, android.accounts.Account[] accountManagerAccounts,
boolean blockExternalChanges, ContentResolver resolver) { boolean blockExternalChanges, ContentResolver resolver) {
boolean accountsDeleted = AccountReconciler.reconcileAccounts(context, boolean accountsDeleted = AccountReconciler.reconcileAccounts(context,

View File

@ -213,7 +213,7 @@ public class NotificationControllerTest extends AndroidTestCase {
Mailbox b1 = ProviderTestUtils.setupMailbox("inbox", a1.mId, true, c, Mailbox.TYPE_INBOX); Mailbox b1 = ProviderTestUtils.setupMailbox("inbox", a1.mId, true, c, Mailbox.TYPE_INBOX);
Message m1 = ProviderTestUtils.setupMessage("message", a1.mId, b1.mId, true, true, c); Message m1 = ProviderTestUtils.setupMessage("message", a1.mId, b1.mId, true, true, c);
n = mTarget.createNewMessageNotification(a1.mId, 1); n = mTarget.createNewMessageNotification(a1.mId, 1, true);
assertEquals(R.drawable.stat_notify_email_generic, n.icon); assertEquals(R.drawable.stat_notify_email_generic, n.icon);
assertEquals(mMockClock.mTime, n.when); assertEquals(mMockClock.mTime, n.when);
@ -223,7 +223,7 @@ public class NotificationControllerTest extends AndroidTestCase {
// TODO Check content -- how? // TODO Check content -- how?
// Case 2: 1 account, 2 unseen message // Case 2: 1 account, 2 unseen message
n = mTarget.createNewMessageNotification(a1.mId, 2); n = mTarget.createNewMessageNotification(a1.mId, 2, true);
assertEquals(R.drawable.stat_notify_email_generic, n.icon); assertEquals(R.drawable.stat_notify_email_generic, n.icon);
assertEquals(mMockClock.mTime, n.when); assertEquals(mMockClock.mTime, n.when);
@ -247,7 +247,7 @@ public class NotificationControllerTest extends AndroidTestCase {
m1.save(c); m1.save(c);
// This shouldn't crash. // This shouldn't crash.
n = mTarget.createNewMessageNotification(a1.mId, 1); n = mTarget.createNewMessageNotification(a1.mId, 1, true);
// Minimum test for the result // Minimum test for the result
assertEquals(R.drawable.stat_notify_email_generic, n.icon); assertEquals(R.drawable.stat_notify_email_generic, n.icon);

View File

@ -260,7 +260,7 @@ public class RefreshManagerTest extends InstrumentationTestCase {
assertTrue(mTarget.isRefreshingAnyMessageListForTest()); assertTrue(mTarget.isRefreshingAnyMessageListForTest());
// Refreshing mailbox 1... // Refreshing mailbox 1...
mController.mListener.updateMailboxCallback(null, ACCOUNT_1, MAILBOX_1, 0, 0); mController.mListener.updateMailboxCallback(null, ACCOUNT_1, MAILBOX_1, 0, 0, null);
assertTrue(mListener.mCalledOnRefreshStatusChanged); assertTrue(mListener.mCalledOnRefreshStatusChanged);
assertFalse(mListener.mCalledOnConnectionError); assertFalse(mListener.mCalledOnConnectionError);
@ -272,7 +272,7 @@ public class RefreshManagerTest extends InstrumentationTestCase {
// Done. // Done.
Log.w(Logging.LOG_TAG, "" + mController.mListener.getClass()); Log.w(Logging.LOG_TAG, "" + mController.mListener.getClass());
mController.mListener.updateMailboxCallback(null, ACCOUNT_1, MAILBOX_1, 100, 0); mController.mListener.updateMailboxCallback(null, ACCOUNT_1, MAILBOX_1, 100, 0, null);
assertTrue(mListener.mCalledOnRefreshStatusChanged); assertTrue(mListener.mCalledOnRefreshStatusChanged);
assertFalse(mListener.mCalledOnConnectionError); assertFalse(mListener.mCalledOnConnectionError);
@ -289,7 +289,7 @@ public class RefreshManagerTest extends InstrumentationTestCase {
// Refreshing mailbox 2... // Refreshing mailbox 2...
mClock.advance(); mClock.advance();
mController.mListener.updateMailboxCallback(null, ACCOUNT_2, MAILBOX_2, 0, 0); mController.mListener.updateMailboxCallback(null, ACCOUNT_2, MAILBOX_2, 0, 0, null);
assertTrue(mListener.mCalledOnRefreshStatusChanged); assertTrue(mListener.mCalledOnRefreshStatusChanged);
assertFalse(mListener.mCalledOnConnectionError); assertFalse(mListener.mCalledOnConnectionError);
@ -300,7 +300,7 @@ public class RefreshManagerTest extends InstrumentationTestCase {
assertEquals(0, mTarget.getMessageListStatusForTest(MAILBOX_2).getLastRefreshTime()); assertEquals(0, mTarget.getMessageListStatusForTest(MAILBOX_2).getLastRefreshTime());
// Done with exception. // Done with exception.
mController.mListener.updateMailboxCallback(EXCEPTION, ACCOUNT_2, MAILBOX_2, 0, 0); mController.mListener.updateMailboxCallback(EXCEPTION, ACCOUNT_2, MAILBOX_2, 0, 0, null);
assertTrue(mListener.mCalledOnRefreshStatusChanged); assertTrue(mListener.mCalledOnRefreshStatusChanged);
assertTrue(mListener.mCalledOnConnectionError); assertTrue(mListener.mCalledOnConnectionError);