From 899c5b866192a4c4a12413446d10e5d98dbf94fa Mon Sep 17 00:00:00 2001 From: Makoto Onuki Date: Sun, 26 Sep 2010 16:16:21 -0700 Subject: [PATCH] Switch to tablet style notification - Now we show separate notification for each account - New notification has sender photo, sender name, and subject of the latest email - Added the NotificationController class, which is intended to manage all notifications besides "new message" eventually. The framework doesn't seem to be 100% ready, and it's not clear how to add the 3rd line in the expanded notification at this point. Need to revisit it later to verify UI details. Change-Id: I40193ee372cb6b2b7245c1588890f238b2469699 --- res/values/strings.xml | 35 +++ .../android/email/NotificationController.java | 227 ++++++++++++++++++ src/com/android/email/SecurityPolicy.java | 5 +- .../email/activity/AccountFolderList.java | 17 +- .../android/email/activity/MessageList.java | 6 +- .../android/email/activity/MessageListXL.java | 5 +- .../activity/setup/AccountSettingsXL.java | 6 +- .../android/email/provider/EmailContent.java | 25 ++ .../service/AttachmentDownloadService.java | 7 +- .../android/email/service/MailService.java | 175 ++++---------- .../android/exchange/CalendarSyncEnabler.java | 5 +- .../android/email/provider/ProviderTests.java | 35 +++ .../exchange/CalendarSyncEnablerTest.java | 6 +- 13 files changed, 395 insertions(+), 159 deletions(-) create mode 100644 src/com/android/email/NotificationController.java diff --git a/res/values/strings.xml b/res/values/strings.xml index 1f1496558..789716c9b 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -163,6 +163,41 @@ in %d accounts + + + %1$s + + %2$d more + %1$s + + %2$d more + + + + + %1$d new + %1$d new + + + + + %1$d new + %2$s + %1$d new + %2$s + + Inbox diff --git a/src/com/android/email/NotificationController.java b/src/com/android/email/NotificationController.java new file mode 100644 index 000000000..dd7e35e89 --- /dev/null +++ b/src/com/android/email/NotificationController.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2010 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; + +import com.android.email.activity.ContactStatusLoader; +import com.android.email.activity.Welcome; +import com.android.email.mail.Address; +import com.android.email.provider.EmailContent; +import com.android.email.provider.EmailContent.Account; +import com.android.email.provider.EmailContent.Message; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.AudioManager; +import android.net.Uri; +import android.text.TextUtils; + +/** + * Class that manages notifications. + * + * TODO Gather all notification related code here + */ +public class NotificationController { + public static final int NOTIFICATION_ID_SECURITY_NEEDED = 1; + public static final int NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED = 2; + public static final int NOTIFICATION_ID_WARNING = 3; + private static final int NOTIFICATION_ID_NEW_MESSAGES_BASE = 10; + + private static NotificationController sInstance; + private final Context mContext; + private final NotificationManager mNotificationManager; + private final AudioManager mAudioManager; + + /** Constructor */ + private NotificationController(Context context) { + mContext = context.getApplicationContext(); + mNotificationManager = (NotificationManager) context.getSystemService( + Context.NOTIFICATION_SERVICE); + mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + } + + /** Singleton access */ + public static synchronized NotificationController getInstance(Context context) { + if (sInstance == null) { + sInstance = new NotificationController(context); + } + return sInstance; + } + + /** + * @return the "new message" notification ID for an account. It just assumes + * accountID won't be too huge. Any other smarter/cleaner way? + */ + private int getNewMessageNotificationId(long accountId) { + return (int) (NOTIFICATION_ID_NEW_MESSAGES_BASE + accountId); + } + + /** + * Dismiss new message notification + * + * @param accountId ID of the target account, or -1 for all accounts. + */ + public void cancelNewMessageNotification(long accountId) { + if (accountId == -1) { + new Utility.ForEachAccount(mContext) { + @Override + protected void performAction(long accountId) { + cancelNewMessageNotification(accountId); + } + }.execute(); + } else { + mNotificationManager.cancel(getNewMessageNotificationId(accountId)); + } + } + + /** + * Show (or update) the "new message" notification. + */ + public void showNewMessageNotification(final long accountId, final int unseenMessageCount, + final int justFetchedCount) { + Utility.runAsync(new Runnable() { + @Override + public void run() { + Notification n = createNewMessageNotification(accountId, unseenMessageCount, + justFetchedCount); + if (n == null) { + return; + } + mNotificationManager.notify(getNewMessageNotificationId(accountId), n); + } + }); + } + + /** + * @return The sender's photo, if available, or null. + * + * Don't call it on the UI thread. + */ + private Bitmap getSenderPhoto(Message message) { + Address sender = Address.unpackFirst(message.mFrom); + if (sender == null) { + return null; + } + String email = sender.getAddress(); + if (TextUtils.isEmpty(email)) { + return null; + } + return ContactStatusLoader.load(mContext, email).mPhoto; + } + + private Bitmap[] getNotificationBitmaps(Bitmap senderPhoto) { + // TODO Should we cache these objects? (bitmaps and arrays) + // They don't have to be on this process's memory once we post a notification request to + // the system, and decodeResource() seems to be reasonably fast. We don't want them to + // take up memory when not necessary. + Bitmap appIcon = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.icon); + if (senderPhoto == null) { + return new Bitmap[] {appIcon}; + } else { + return new Bitmap[] {senderPhoto, appIcon}; + } + } + + /** + * Create a notification + * + * Don't call it on the UI thread. + * + * TODO Test it when the UI is settled. + */ + private Notification createNewMessageNotification(long accountId, int unseenMessageCount, + int justFetchedCount) { + final Account account = Account.restoreAccountWithId(mContext, accountId); + if (account == null) { + return null; + } + // Get the latest message + final Message message = Message.getLatestMessage(mContext, accountId); + if (message == null) { + return null; // no message found??? + } + + final String senderName = Address.toFriendly(Address.unpack(message.mFrom)); + final String subject = message.mSubject; + final Bitmap senderPhoto = getSenderPhoto(message); + + // Intent to open inbox + PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0, + Welcome.createOpenAccountInboxIntent(mContext, accountId), + PendingIntent.FLAG_UPDATE_CURRENT); + + final String notificationTitle; + if (justFetchedCount == 1) { + notificationTitle = senderName; + } else { + notificationTitle = mContext.getResources().getQuantityString( + R.plurals.notification_sender_name_multi_messages, justFetchedCount - 1, + senderName, justFetchedCount - 1); + } + final String content = subject; + final String numNewMessages; + final int numAccounts = EmailContent.count(mContext, Account.CONTENT_URI); + if (numAccounts == 1) { + numNewMessages = mContext.getResources().getQuantityString( + R.plurals.notification_num_new_messages_single_account, unseenMessageCount, + unseenMessageCount, account.mDisplayName); + } else { + numNewMessages = mContext.getResources().getQuantityString( + R.plurals.notification_num_new_messages_multi_account, unseenMessageCount, + unseenMessageCount, account.mDisplayName); + } + + Notification notification = new Notification(R.drawable.stat_notify_email_generic, + mContext.getString(R.string.notification_new_title), System.currentTimeMillis()); + notification.setLatestEventInfo(mContext, notificationTitle, subject, contentIntent); + + notification.tickerTitle = notificationTitle; + // STOPSHIPO numNewMessages should be the 3rd line on expanded notification. But it's not + // clear how to do that yet. + // For now we just append it to subject. + notification.tickerSubtitle = subject + " " + numNewMessages; + notification.tickerIcons = getNotificationBitmaps(senderPhoto); + + setupNotificationSoundAndVibrationFromAccount(notification, account); + return notification; + } + + private boolean isRingerModeSilent() { + return mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL; + } + + private void setupNotificationSoundAndVibrationFromAccount(Notification notification, + Account account) { + final int flags = account.mFlags; + final String ringtoneUri = account.mRingtoneUri; + final boolean vibrate = (flags & Account.FLAGS_VIBRATE_ALWAYS) != 0; + final boolean vibrateWhenSilent = (flags & Account.FLAGS_VIBRATE_WHEN_SILENT) != 0; + + notification.sound = (ringtoneUri == null) ? null : Uri.parse(ringtoneUri); + + if (vibrate || (vibrateWhenSilent && isRingerModeSilent())) { + notification.defaults |= Notification.DEFAULT_VIBRATE; + } + + // This code is identical to that used by Gmail and GTalk for notifications + notification.flags |= Notification.FLAG_SHOW_LIGHTS; + notification.defaults |= Notification.DEFAULT_LIGHTS; + } +} diff --git a/src/com/android/email/SecurityPolicy.java b/src/com/android/email/SecurityPolicy.java index 4daaf7236..5f9250016 100644 --- a/src/com/android/email/SecurityPolicy.java +++ b/src/com/android/email/SecurityPolicy.java @@ -363,7 +363,8 @@ public class SecurityPolicy { NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.notify(MailService.NOTIFICATION_ID_SECURITY_NEEDED, notification); + notificationManager.notify(NotificationController.NOTIFICATION_ID_SECURITY_NEEDED, + notification); } /** @@ -373,7 +374,7 @@ public class SecurityPolicy { public void clearNotification(long accountId) { NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(MailService.NOTIFICATION_ID_SECURITY_NEEDED); + notificationManager.cancel(NotificationController.NOTIFICATION_ID_SECURITY_NEEDED); } /** diff --git a/src/com/android/email/activity/AccountFolderList.java b/src/com/android/email/activity/AccountFolderList.java index cdf92e08b..8af544b75 100644 --- a/src/com/android/email/activity/AccountFolderList.java +++ b/src/com/android/email/activity/AccountFolderList.java @@ -19,6 +19,7 @@ package com.android.email.activity; import com.android.email.Controller; import com.android.email.ControllerResultUiThreadWrapper; import com.android.email.Email; +import com.android.email.NotificationController; import com.android.email.R; import com.android.email.activity.setup.AccountSettingsXL; import com.android.email.activity.setup.AccountSetupBasics; @@ -26,7 +27,6 @@ import com.android.email.mail.MessagingException; import com.android.email.provider.EmailContent; import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.Mailbox; -import com.android.email.service.MailService; import android.app.Activity; import android.app.AlertDialog; @@ -37,7 +37,6 @@ import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.os.Handler; -import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -190,6 +189,7 @@ public class AccountFolderList extends Activity implements AccountFolderListFrag } private Dialog createRemoveAccountDialog() { + final Activity activity = this; return new AlertDialog.Builder(this) .setIcon(android.R.drawable.ic_dialog_alert) .setTitle(R.string.account_delete_dlg_title) @@ -197,17 +197,18 @@ public class AccountFolderList extends Activity implements AccountFolderListFrag mSelectedContextAccount.getDisplayName())) .setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { + final long accountId = mSelectedContextAccount.mId; dismissDialog(DIALOG_REMOVE_ACCOUNT); - // Clear notifications, which may become stale here - MailService.cancelNewMessageNotification(AccountFolderList.this); - int numAccounts = EmailContent.count(AccountFolderList.this, + // Dismiss new message notification. + NotificationController.getInstance(activity) + .cancelNewMessageNotification(accountId); + int numAccounts = EmailContent.count(activity, Account.CONTENT_URI, null, null); mListFragment.hideDeletingAccount(mSelectedContextAccount.mId); - Controller.getInstance(AccountFolderList.this).deleteAccount( - mSelectedContextAccount.mId); + Controller.getInstance(activity).deleteAccount(accountId); if (numAccounts == 1) { - AccountSetupBasics.actionNewAccount(AccountFolderList.this); + AccountSetupBasics.actionNewAccount(activity); finish(); } } diff --git a/src/com/android/email/activity/MessageList.java b/src/com/android/email/activity/MessageList.java index 8f97cc9fa..6a8065612 100644 --- a/src/com/android/email/activity/MessageList.java +++ b/src/com/android/email/activity/MessageList.java @@ -19,6 +19,7 @@ package com.android.email.activity; import com.android.email.Controller; import com.android.email.ControllerResultUiThreadWrapper; import com.android.email.Email; +import com.android.email.NotificationController; import com.android.email.R; import com.android.email.Utility; import com.android.email.activity.setup.AccountSecurity; @@ -29,10 +30,8 @@ import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.AccountColumns; import com.android.email.provider.EmailContent.Mailbox; import com.android.email.provider.EmailContent.MailboxColumns; -import com.android.email.service.MailService; import android.app.Activity; -import android.app.NotificationManager; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; @@ -237,7 +236,8 @@ public class MessageList extends Activity implements OnClickListener, mController.addResultCallback(mControllerCallback); // clear notifications here - MailService.cancelNewMessageNotification(this); + NotificationController.getInstance(this).cancelNewMessageNotification( + mListFragment.getAccountId()); // Exit immediately if the accounts list has changed (e.g. externally deleted) if (Email.getNotifyUiAccountsChanged()) { diff --git a/src/com/android/email/activity/MessageListXL.java b/src/com/android/email/activity/MessageListXL.java index 69922341d..eda5b0321 100644 --- a/src/com/android/email/activity/MessageListXL.java +++ b/src/com/android/email/activity/MessageListXL.java @@ -18,6 +18,7 @@ package com.android.email.activity; import com.android.email.Clock; import com.android.email.Email; +import com.android.email.NotificationController; import com.android.email.Preferences; import com.android.email.R; import com.android.email.RefreshManager; @@ -26,7 +27,6 @@ import com.android.email.activity.setup.AccountSecurity; import com.android.email.activity.setup.AccountSettingsXL; import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.Mailbox; -import com.android.email.service.MailService; import android.app.ActionBar; import android.app.Activity; @@ -174,7 +174,8 @@ public class MessageListXL extends Activity implements if (Email.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Email.LOG_TAG, "MessageListXL onResume"); super.onResume(); - MailService.cancelNewMessageNotification(this); + NotificationController.getInstance(this).cancelNewMessageNotification( + mFragmentManager.getAccountId()); // On MessageList.onResume, we go back to Welcome if an account has been added/removed. // We don't need to do that here, because when the activity resumes, the account list loader diff --git a/src/com/android/email/activity/setup/AccountSettingsXL.java b/src/com/android/email/activity/setup/AccountSettingsXL.java index a5c5a5d5d..fdf4e9ea3 100644 --- a/src/com/android/email/activity/setup/AccountSettingsXL.java +++ b/src/com/android/email/activity/setup/AccountSettingsXL.java @@ -18,6 +18,7 @@ package com.android.email.activity.setup; import com.android.email.Controller; import com.android.email.Email; +import com.android.email.NotificationController; import com.android.email.R; import com.android.email.Utility; import com.android.email.mail.Sender; @@ -25,7 +26,6 @@ import com.android.email.mail.Store; import com.android.email.provider.EmailContent; import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.AccountColumns; -import com.android.email.service.MailService; import android.app.Activity; import android.app.AlertDialog; @@ -558,8 +558,8 @@ public class AccountSettingsXL extends PreferenceActivity implements OnClickList public void deleteAccount(Account account) { // Kick off the work to actually delete the account - // Clear notifications, which may become stale here - MailService.cancelNewMessageNotification(this); + // Clear notifications for the account. + NotificationController.getInstance(this).cancelNewMessageNotification(account.mId); // Delete the account (note, this is async. Would be nice to get a callback. Controller.getInstance(this).deleteAccount(account.mId); diff --git a/src/com/android/email/provider/EmailContent.java b/src/com/android/email/provider/EmailContent.java index 110b0ebed..53f4eec62 100644 --- a/src/com/android/email/provider/EmailContent.java +++ b/src/com/android/email/provider/EmailContent.java @@ -32,6 +32,7 @@ import android.os.Environment; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; +import android.text.TextUtils; import java.io.File; import java.net.URI; @@ -450,6 +451,7 @@ public abstract class EmailContent { // To refer to a specific message, use ContentUris.withAppendedId(CONTENT_URI, id) @SuppressWarnings("hiding") public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/message"); + public static final Uri CONTENT_URI_LIMIT_1 = uriWithLimit(CONTENT_URI, 1); public static final Uri SYNCED_CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/syncedMessage"); public static final Uri DELETED_CONTENT_URI = @@ -539,6 +541,9 @@ public abstract class EmailContent { private static final String FAVORITE_COUNT_SELECTION = MessageColumns.FLAG_FAVORITE + "= 1"; + private static final String ACCOUNT_KEY_SELECTION = + MessageColumns.ACCOUNT_KEY + "=?"; + // _id field is in AbstractContent public String mDisplayName; public long mTimeStamp; @@ -829,6 +834,26 @@ public abstract class EmailContent { } return -1; } + + /** + * @return the latest messages on an account. + */ + public static Message getLatestMessage(Context context, Long accountId) { + Cursor c = context.getContentResolver().query(Message.CONTENT_URI_LIMIT_1, + Message.CONTENT_PROJECTION, + ACCOUNT_KEY_SELECTION, new String[] {Long.toString(accountId)}, + EmailContent.MessageColumns.TIMESTAMP + " DESC"); + try { + if (c.moveToFirst()) { + Message m = new Message(); + m.restore(c); + return m; + } + } finally { + c.close(); + } + return null; // not found; + } } public interface AccountColumns { diff --git a/src/com/android/email/service/AttachmentDownloadService.java b/src/com/android/email/service/AttachmentDownloadService.java index f59748d7f..2e0017f11 100644 --- a/src/com/android/email/service/AttachmentDownloadService.java +++ b/src/com/android/email/service/AttachmentDownloadService.java @@ -16,11 +16,12 @@ package com.android.email.service; +import com.android.email.Controller.ControllerService; import com.android.email.Email; +import com.android.email.ExchangeUtils.NullEmailService; +import com.android.email.NotificationController; import com.android.email.R; import com.android.email.Utility; -import com.android.email.Controller.ControllerService; -import com.android.email.ExchangeUtils.NullEmailService; import com.android.email.activity.Welcome; import com.android.email.provider.AttachmentProvider; import com.android.email.provider.EmailContent; @@ -466,7 +467,7 @@ public class AttachmentDownloadService extends Service implements Runnable { n.flags = Notification.FLAG_AUTO_CANCEL; NotificationManager nm = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(MailService.NOTIFICATION_ID_WARNING, n); + nm.notify(NotificationController.NOTIFICATION_ID_WARNING, n); } /** diff --git a/src/com/android/email/service/MailService.java b/src/com/android/email/service/MailService.java index cb775c399..1f36993a1 100644 --- a/src/com/android/email/service/MailService.java +++ b/src/com/android/email/service/MailService.java @@ -19,16 +19,20 @@ package com.android.email.service; import com.android.email.AccountBackupRestore; import com.android.email.Controller; import com.android.email.Email; +import com.android.email.NotificationController; import com.android.email.R; import com.android.email.SecurityPolicy; import com.android.email.Utility; +import com.android.email.activity.ContactStatusLoader; import com.android.email.activity.Welcome; +import com.android.email.mail.Address; import com.android.email.mail.MessagingException; import com.android.email.provider.EmailContent; import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.AccountColumns; import com.android.email.provider.EmailContent.HostAuth; import com.android.email.provider.EmailContent.Mailbox; +import com.android.email.provider.EmailContent.Message; import com.android.email.provider.EmailProvider; import android.accounts.AccountManager; @@ -44,11 +48,12 @@ import android.app.PendingIntent; import android.app.Service; import android.content.ContentResolver; import android.content.ContentUris; -import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.SyncStatusObserver; import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.media.AudioManager; import android.net.ConnectivityManager; import android.net.Uri; @@ -56,6 +61,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.SystemClock; +import android.text.TextUtils; import android.util.Config; import android.util.Log; @@ -73,11 +79,6 @@ public class MailService extends Service { private static final String LOG_TAG = "Email-MailService"; - private static final int NOTIFICATION_ID_NEW_MESSAGES = 1; - public static final int NOTIFICATION_ID_SECURITY_NEEDED = 2; - public static final int NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED = 3; - public static final int NOTIFICATION_ID_WARNING = 4; - private static final String ACTION_CHECK_MAIL = "com.android.email.intent.action.MAIL_SERVICE_WAKEUP"; private static final String ACTION_RESCHEDULE = @@ -149,7 +150,7 @@ public class MailService extends Service { synchronized (mSyncReports) { for (AccountSyncReport report : mSyncReports.values()) { if (accountId == -1 || accountId == report.accountId) { - report.numNewMessages = 0; + report.unseenMessageCount = 0; } } } @@ -182,12 +183,6 @@ public class MailService extends Service { context.startService(i); } - public static void cancelNewMessageNotification(Context context) { - NotificationManager notificationManager = (NotificationManager) - context.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(MailService.NOTIFICATION_ID_NEW_MESSAGES); - } - @Override public int onStartCommand(Intent intent, int flags, int startId) { super.onStartCommand(intent, flags, startId); @@ -282,7 +277,7 @@ public class MailService extends Service { // As a precaution, clear any outstanding Email notifications // We could be smarter and only do this when the list of accounts changes, // but that's an edge condition and this is much safer. - cancelNewMessageNotification(this); + NotificationController.getInstance(this).cancelNewMessageNotification(-1); // When called externally, we refresh the sync reports table to pick up // any changes in the account list or account settings @@ -496,22 +491,34 @@ public class MailService extends Service { long accountId; long prevSyncTime; // 0 == unknown long nextSyncTime; // 0 == ASAP -1 == don't sync - int numNewMessages; + + /** # of "unseen" messages to show in notification */ + int unseenMessageCount; + + /** + * # of unseen, the value shown on the last notification. Used to + * 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; int syncInterval; boolean notify; - boolean vibrate; - boolean vibrateWhenSilent; - Uri ringtoneUri; - String accountDisplayName; boolean syncEnabled; // whether auto sync is enabled for this account + /** # of messages that have just been fetched */ + int getJustFetchedMessageCount() { + return unseenMessageCount - lastUnseenMessageCount; + } @Override public String toString() { - return accountDisplayName + ": id=" + accountId + " prevSync=" + prevSyncTime - + " nextSync=" + nextSyncTime + " numNew=" + numNewMessages; + return "id=" + accountId + + " prevSync=" + prevSyncTime + " nextSync=" + nextSyncTime + " numUnseen=" + + unseenMessageCount; } } @@ -560,7 +567,6 @@ public class MailService extends Service { int syncInterval = c.getInt(Account.CONTENT_SYNC_INTERVAL_COLUMN); int flags = c.getInt(Account.CONTENT_FLAGS_COLUMN); long id = c.getInt(Account.CONTENT_ID_COLUMN); - String ringtoneString = c.getString(Account.CONTENT_RINGTONE_URI_COLUMN); // If we're not using MessagingController (EAS at this point), don't schedule syncs if (!mController.isMessagingController(id)) { @@ -572,16 +578,10 @@ public class MailService extends Service { report.accountId = c.getLong(Account.CONTENT_ID_COLUMN); report.prevSyncTime = 0; report.nextSyncTime = (syncInterval > 0) ? 0 : -1; // 0 == ASAP -1 == no sync - report.numNewMessages = 0; + report.unseenMessageCount = 0; report.syncInterval = syncInterval; report.notify = (flags & Account.FLAGS_NOTIFY_NEW_MAIL) != 0; - report.vibrate = (flags & Account.FLAGS_VIBRATE_ALWAYS) != 0; - report.vibrateWhenSilent = (flags & Account.FLAGS_VIBRATE_WHEN_SILENT) != 0; - report.ringtoneUri = (ringtoneString == null) ? null - : Uri.parse(ringtoneString); - - report.accountDisplayName = c.getString(Account.CONTENT_DISPLAY_NAME_COLUMN); // See if the account is enabled for sync in AccountManager Account providerAccount = Account.restoreAccountWithId(this, id); @@ -623,7 +623,7 @@ public class MailService extends Service { report.nextSyncTime = report.prevSyncTime + (report.syncInterval * 1000 * 60); } if (newCount != -1) { - report.numNewMessages = newCount; + report.unseenMessageCount = newCount; } if (Config.LOGD && Email.DEBUG) { Log.d(LOG_TAG, "update account " + report.toString()); @@ -709,114 +709,23 @@ public class MailService extends Service { } /** - * Prepare notifications for a given new account having received mail - * The notification is organized around the account that has the new mail (e.g. selecting - * the alert preferences) but the notification will include a summary if other - * accounts also have new mail. + * Show "new message" notification for an account. (Notification is shown per account.) */ - private void notifyNewMessages(long accountId) { - boolean notify = false; - boolean vibrate = false; - boolean vibrateWhenSilent = false; - Uri ringtone = null; - int accountsWithNewMessages = 0; - int numNewMessages = 0; - String reportName = null; + private void notifyNewMessages(final long accountId) { + final int unseenMessageCount; + final int justFetchedCount; synchronized (mSyncReports) { - for (AccountSyncReport report : mSyncReports.values()) { - if (report.numNewMessages == 0) { - continue; - } - numNewMessages += report.numNewMessages; - accountsWithNewMessages += 1; - if (report.accountId == accountId) { - notify = report.notify; - vibrate = report.vibrate; - vibrateWhenSilent = report.vibrateWhenSilent; - ringtone = report.ringtoneUri; - reportName = report.accountDisplayName; - } + AccountSyncReport report = mSyncReports.get(accountId); + if (report == null || report.unseenMessageCount == 0 || !report.notify) { + return; } - } - if (!notify) { - return; + unseenMessageCount = report.unseenMessageCount; + justFetchedCount = report.getJustFetchedMessageCount(); + report.lastUnseenMessageCount = report.unseenMessageCount; } - showNewMessageNotification(this, accountId, vibrate, vibrateWhenSilent, ringtone, - accountsWithNewMessages, numNewMessages, reportName); - } - - /** Simply runs {@link #showNewMessageNotificationInternal} on a worker thread. */ - private static void showNewMessageNotification(final Context context, - final long accountId, final boolean vibrate, final boolean vibrateWhenSilent, - final Uri ringtone, final int accountsWithNewMessages, final int numNewMessages, - final String reportName) { - Utility.runAsync(new Runnable() { - @Override - public void run() { - showNewMessageNotificationInternal(context, accountId, vibrate, vibrateWhenSilent, - ringtone, accountsWithNewMessages, numNewMessages, reportName); - } - }); - } - - /** - * Show (or update) a notification. - * - * TODO Implement new style notification. (show sender photo, sender name, subject, ...) - */ - private static void showNewMessageNotificationInternal(Context context, long accountId, - boolean vibrate, boolean vibrateWhenSilent, Uri ringtone, int accountsWithNewMessages, - int numNewMessages, String reportName) { - // set up to post a notification - Intent intent; - String reportString; - - if (accountsWithNewMessages == 1) { - // Prepare a report for a single account - // "12 unread (gmail)" - reportString = context.getResources().getQuantityString( - R.plurals.notification_new_one_account_fmt, numNewMessages, - numNewMessages, reportName); - intent = Welcome.createOpenAccountInboxIntent(context, accountId); - } else { - // Prepare a report for multiple accounts - // "4 accounts" - reportString = context.getResources().getQuantityString( - R.plurals.notification_new_multi_account_fmt, accountsWithNewMessages, - accountsWithNewMessages); - intent = Welcome.createOpenCombinedInboxIntent(context); - } - - // prepare appropriate pending intent, set up notification, and send - PendingIntent pending = - PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); - - Notification notification = new Notification( - R.drawable.stat_notify_email_generic, - context.getString(R.string.notification_new_title), - System.currentTimeMillis()); - notification.setLatestEventInfo(context, - context.getString(R.string.notification_new_title), - reportString, - pending); - - notification.sound = ringtone; - AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - boolean nowSilent = audioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL; - - // Use same code here as in Gmail and GTalk for vibration - if (vibrate || (vibrateWhenSilent && nowSilent)) { - notification.defaults |= Notification.DEFAULT_VIBRATE; - } - - // This code is identical to that used by Gmail and GTalk for notifications - notification.flags |= Notification.FLAG_SHOW_LIGHTS; - notification.defaults |= Notification.DEFAULT_LIGHTS; - - NotificationManager notificationManager = - (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.notify(NOTIFICATION_ID_NEW_MESSAGES, notification); + NotificationController.getInstance(this).showNewMessageNotification(accountId, + unseenMessageCount, justFetchedCount); } /** diff --git a/src/com/android/exchange/CalendarSyncEnabler.java b/src/com/android/exchange/CalendarSyncEnabler.java index 4eaad8eec..5837bb2d2 100644 --- a/src/com/android/exchange/CalendarSyncEnabler.java +++ b/src/com/android/exchange/CalendarSyncEnabler.java @@ -17,8 +17,8 @@ package com.android.exchange; import com.android.email.Email; +import com.android.email.NotificationController; import com.android.email.R; -import com.android.email.service.MailService; import android.accounts.Account; import android.accounts.AccountManager; @@ -30,7 +30,6 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; import android.provider.Calendar; -import android.provider.ContactsContract; import android.util.Log; /** @@ -106,7 +105,7 @@ public class CalendarSyncEnabler { NotificationManager nm = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(MailService.NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED, n); + nm.notify(NotificationController.NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED, n); } /** @return {@link Intent} to launch the Calendar app. */ diff --git a/tests/src/com/android/email/provider/ProviderTests.java b/tests/src/com/android/email/provider/ProviderTests.java index 4af4cea80..6ec8d9a38 100644 --- a/tests/src/com/android/email/provider/ProviderTests.java +++ b/tests/src/com/android/email/provider/ProviderTests.java @@ -2210,4 +2210,39 @@ public class ProviderTests extends ProviderTestCase2 { assertEquals(0, Account.restoreAccountWithId(c, a4.mId).mNewMessageCount); 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 testMessageGetLatestMessage() { + 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 b2 = ProviderTestUtils.setupMailbox("box3", a2.mId, true, c, Mailbox.TYPE_INBOX); + + // Create some messages + Message m11 = createMessageWithTimestamp(c, b1, 33); + Message m12 = createMessageWithTimestamp(c, b1, 10); + Message m13 = createMessageWithTimestamp(c, b1, 1000); + + Message m21 = createMessageWithTimestamp(c, b2, 99); + Message m22 = createMessageWithTimestamp(c, b2, 1); + Message m23 = createMessageWithTimestamp(c, b2, 2); + + // Check! + assertEquals(m13.mId, Message.getLatestMessage(c, a1.mId).mId); + assertEquals(m21.mId, Message.getLatestMessage(c, a2.mId).mId); + + // No such account + assertEquals(null, Message.getLatestMessage(c, 9999999L)); + } } diff --git a/tests/src/com/android/exchange/CalendarSyncEnablerTest.java b/tests/src/com/android/exchange/CalendarSyncEnablerTest.java index 16b9b28ab..b244c93bb 100644 --- a/tests/src/com/android/exchange/CalendarSyncEnablerTest.java +++ b/tests/src/com/android/exchange/CalendarSyncEnablerTest.java @@ -18,7 +18,7 @@ package com.android.exchange; import com.android.email.AccountTestCase; import com.android.email.Email; -import com.android.email.service.MailService; +import com.android.email.NotificationController; import android.accounts.Account; import android.accounts.AccountManager; @@ -193,7 +193,9 @@ public class CalendarSyncEnablerTest extends AccountTestCase { enabler.showNotification("a@b.com"); // Remove the notification. Comment it out when you want to know how it looks like. + // TODO If NotificationController supports this notification, we can just mock it out + // and remove this code. ((NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE)) - .cancel(MailService.NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED); + .cancel(NotificationController.NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED); } }