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
This commit is contained in:
Makoto Onuki 2010-09-26 16:16:21 -07:00
parent cd2e98d000
commit 899c5b8661
13 changed files with 395 additions and 159 deletions

View File

@ -163,6 +163,41 @@
<item quantity="other">in <xliff:g id="number_accounts" example="10">%d</xliff:g> accounts</item> <item quantity="other">in <xliff:g id="number_accounts" example="10">%d</xliff:g> accounts</item>
</plurals> </plurals>
<!-- "new message" notification message title.
"sender_name" will be replaced with the sender name of the latest email,
and "num_more_mails" will be replaced with the number of other messages that are received
together.
[CHAR LIMIT=none] -->
<plurals name="notification_sender_name_multi_messages">
<item quantity="one"><xliff:g id="sender_name">%1$s</xliff:g>
+ <xliff:g id="num_more_mails">%2$d</xliff:g> more</item>
<item quantity="other"><xliff:g id="sender_name">%1$s</xliff:g>
+ <xliff:g id="num_more_mails">%2$d</xliff:g> more</item>
</plurals>
<!-- "new message" notification body for the number of new messages,
used when only one account is set up.
"num_new_message" will be replaced with the number of new messages. [CHAR LIMIT=none] -->
<plurals name="notification_num_new_messages_single_account">
<item quantity="one"><xliff:g id="num_new_message">%1$d</xliff:g></item> new
<item quantity="other"><xliff:g id="num_new_message">%1$d</xliff:g></item> new
</plurals>
<!-- "new message" notification body for the number of new messages,
used when multiple accounts are set up.
"num_new_message" will be replaced with the number of new messages, and "account_name"
will be replaced with the receiving account name.
"separator" should be between these two elements.
[CHAR LIMIT=none] -->
<plurals name="notification_num_new_messages_multi_account">
<item quantity="one"><xliff:g id="num_new_message">%1$d</xliff:g
> new<xliff:g id="separator">&#8195;</xliff:g>
<xliff:g id="account_name">%2$s</xliff:g></item>
<item quantity="other"><xliff:g id="num_new_message">%1$d</xliff:g
> new<xliff:g id="separator">&#8195;</xliff:g>
<xliff:g id="account_name">%2$s</xliff:g></item>
</plurals>
<!-- The next set of strings are used server-side and must not be localized. --> <!-- The next set of strings are used server-side and must not be localized. -->
<!-- Do Not Translate. This is the name of the "inbox" folder, on the server. --> <!-- Do Not Translate. This is the name of the "inbox" folder, on the server. -->
<string name="mailbox_name_server_inbox" translatable="false">Inbox</string> <string name="mailbox_name_server_inbox" translatable="false">Inbox</string>

View File

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

View File

@ -363,7 +363,8 @@ public class SecurityPolicy {
NotificationManager notificationManager = NotificationManager notificationManager =
(NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); (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) { public void clearNotification(long accountId) {
NotificationManager notificationManager = NotificationManager notificationManager =
(NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(MailService.NOTIFICATION_ID_SECURITY_NEEDED); notificationManager.cancel(NotificationController.NOTIFICATION_ID_SECURITY_NEEDED);
} }
/** /**

View File

@ -19,6 +19,7 @@ package com.android.email.activity;
import com.android.email.Controller; 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.NotificationController;
import com.android.email.R; import com.android.email.R;
import com.android.email.activity.setup.AccountSettingsXL; import com.android.email.activity.setup.AccountSettingsXL;
import com.android.email.activity.setup.AccountSetupBasics; 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;
import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.Account;
import com.android.email.provider.EmailContent.Mailbox; import com.android.email.provider.EmailContent.Mailbox;
import com.android.email.service.MailService;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
@ -37,7 +37,6 @@ import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.view.KeyEvent;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
@ -190,6 +189,7 @@ public class AccountFolderList extends Activity implements AccountFolderListFrag
} }
private Dialog createRemoveAccountDialog() { private Dialog createRemoveAccountDialog() {
final Activity activity = this;
return new AlertDialog.Builder(this) return new AlertDialog.Builder(this)
.setIcon(android.R.drawable.ic_dialog_alert) .setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.account_delete_dlg_title) .setTitle(R.string.account_delete_dlg_title)
@ -197,17 +197,18 @@ public class AccountFolderList extends Activity implements AccountFolderListFrag
mSelectedContextAccount.getDisplayName())) mSelectedContextAccount.getDisplayName()))
.setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() { .setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) { public void onClick(DialogInterface dialog, int whichButton) {
final long accountId = mSelectedContextAccount.mId;
dismissDialog(DIALOG_REMOVE_ACCOUNT); dismissDialog(DIALOG_REMOVE_ACCOUNT);
// Clear notifications, which may become stale here // Dismiss new message notification.
MailService.cancelNewMessageNotification(AccountFolderList.this); NotificationController.getInstance(activity)
int numAccounts = EmailContent.count(AccountFolderList.this, .cancelNewMessageNotification(accountId);
int numAccounts = EmailContent.count(activity,
Account.CONTENT_URI, null, null); Account.CONTENT_URI, null, null);
mListFragment.hideDeletingAccount(mSelectedContextAccount.mId); mListFragment.hideDeletingAccount(mSelectedContextAccount.mId);
Controller.getInstance(AccountFolderList.this).deleteAccount( Controller.getInstance(activity).deleteAccount(accountId);
mSelectedContextAccount.mId);
if (numAccounts == 1) { if (numAccounts == 1) {
AccountSetupBasics.actionNewAccount(AccountFolderList.this); AccountSetupBasics.actionNewAccount(activity);
finish(); finish();
} }
} }

View File

@ -19,6 +19,7 @@ package com.android.email.activity;
import com.android.email.Controller; 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.NotificationController;
import com.android.email.R; import com.android.email.R;
import com.android.email.Utility; import com.android.email.Utility;
import com.android.email.activity.setup.AccountSecurity; 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.AccountColumns;
import com.android.email.provider.EmailContent.Mailbox; import com.android.email.provider.EmailContent.Mailbox;
import com.android.email.provider.EmailContent.MailboxColumns; import com.android.email.provider.EmailContent.MailboxColumns;
import com.android.email.service.MailService;
import android.app.Activity; import android.app.Activity;
import android.app.NotificationManager;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@ -237,7 +236,8 @@ public class MessageList extends Activity implements OnClickListener,
mController.addResultCallback(mControllerCallback); mController.addResultCallback(mControllerCallback);
// clear notifications here // clear notifications here
MailService.cancelNewMessageNotification(this); NotificationController.getInstance(this).cancelNewMessageNotification(
mListFragment.getAccountId());
// Exit immediately if the accounts list has changed (e.g. externally deleted) // Exit immediately if the accounts list has changed (e.g. externally deleted)
if (Email.getNotifyUiAccountsChanged()) { if (Email.getNotifyUiAccountsChanged()) {

View File

@ -18,6 +18,7 @@ package com.android.email.activity;
import com.android.email.Clock; import com.android.email.Clock;
import com.android.email.Email; import com.android.email.Email;
import com.android.email.NotificationController;
import com.android.email.Preferences; import com.android.email.Preferences;
import com.android.email.R; import com.android.email.R;
import com.android.email.RefreshManager; 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.activity.setup.AccountSettingsXL;
import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.Account;
import com.android.email.provider.EmailContent.Mailbox; import com.android.email.provider.EmailContent.Mailbox;
import com.android.email.service.MailService;
import android.app.ActionBar; import android.app.ActionBar;
import android.app.Activity; 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"); if (Email.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Email.LOG_TAG, "MessageListXL onResume");
super.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. // 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 // We don't need to do that here, because when the activity resumes, the account list loader

View File

@ -18,6 +18,7 @@ package com.android.email.activity.setup;
import com.android.email.Controller; import com.android.email.Controller;
import com.android.email.Email; import com.android.email.Email;
import com.android.email.NotificationController;
import com.android.email.R; import com.android.email.R;
import com.android.email.Utility; import com.android.email.Utility;
import com.android.email.mail.Sender; 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;
import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.Account;
import com.android.email.provider.EmailContent.AccountColumns; import com.android.email.provider.EmailContent.AccountColumns;
import com.android.email.service.MailService;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
@ -558,8 +558,8 @@ public class AccountSettingsXL extends PreferenceActivity implements OnClickList
public void deleteAccount(Account account) { public void deleteAccount(Account account) {
// Kick off the work to actually delete the account // Kick off the work to actually delete the account
// Clear notifications, which may become stale here // Clear notifications for the account.
MailService.cancelNewMessageNotification(this); NotificationController.getInstance(this).cancelNewMessageNotification(account.mId);
// Delete the account (note, this is async. Would be nice to get a callback. // Delete the account (note, this is async. Would be nice to get a callback.
Controller.getInstance(this).deleteAccount(account.mId); Controller.getInstance(this).deleteAccount(account.mId);

View File

@ -32,6 +32,7 @@ import android.os.Environment;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.os.RemoteException; import android.os.RemoteException;
import android.text.TextUtils;
import java.io.File; import java.io.File;
import java.net.URI; import java.net.URI;
@ -450,6 +451,7 @@ public abstract class EmailContent {
// To refer to a specific message, use ContentUris.withAppendedId(CONTENT_URI, id) // To refer to a specific message, use ContentUris.withAppendedId(CONTENT_URI, id)
@SuppressWarnings("hiding") @SuppressWarnings("hiding")
public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/message"); 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 = public static final Uri SYNCED_CONTENT_URI =
Uri.parse(EmailContent.CONTENT_URI + "/syncedMessage"); Uri.parse(EmailContent.CONTENT_URI + "/syncedMessage");
public static final Uri DELETED_CONTENT_URI = public static final Uri DELETED_CONTENT_URI =
@ -539,6 +541,9 @@ public abstract class EmailContent {
private static final String FAVORITE_COUNT_SELECTION = private static final String FAVORITE_COUNT_SELECTION =
MessageColumns.FLAG_FAVORITE + "= 1"; MessageColumns.FLAG_FAVORITE + "= 1";
private static final String ACCOUNT_KEY_SELECTION =
MessageColumns.ACCOUNT_KEY + "=?";
// _id field is in AbstractContent // _id field is in AbstractContent
public String mDisplayName; public String mDisplayName;
public long mTimeStamp; public long mTimeStamp;
@ -829,6 +834,26 @@ public abstract class EmailContent {
} }
return -1; 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 { public interface AccountColumns {

View File

@ -16,11 +16,12 @@
package com.android.email.service; package com.android.email.service;
import com.android.email.Controller.ControllerService;
import com.android.email.Email; 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.R;
import com.android.email.Utility; 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.activity.Welcome;
import com.android.email.provider.AttachmentProvider; import com.android.email.provider.AttachmentProvider;
import com.android.email.provider.EmailContent; import com.android.email.provider.EmailContent;
@ -466,7 +467,7 @@ public class AttachmentDownloadService extends Service implements Runnable {
n.flags = Notification.FLAG_AUTO_CANCEL; n.flags = Notification.FLAG_AUTO_CANCEL;
NotificationManager nm = NotificationManager nm =
(NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify(MailService.NOTIFICATION_ID_WARNING, n); nm.notify(NotificationController.NOTIFICATION_ID_WARNING, n);
} }
/** /**

View File

@ -19,16 +19,20 @@ package com.android.email.service;
import com.android.email.AccountBackupRestore; import com.android.email.AccountBackupRestore;
import com.android.email.Controller; import com.android.email.Controller;
import com.android.email.Email; import com.android.email.Email;
import com.android.email.NotificationController;
import com.android.email.R; import com.android.email.R;
import com.android.email.SecurityPolicy; import com.android.email.SecurityPolicy;
import com.android.email.Utility; import com.android.email.Utility;
import com.android.email.activity.ContactStatusLoader;
import com.android.email.activity.Welcome; import com.android.email.activity.Welcome;
import com.android.email.mail.Address;
import com.android.email.mail.MessagingException; import com.android.email.mail.MessagingException;
import com.android.email.provider.EmailContent; import com.android.email.provider.EmailContent;
import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.Account;
import com.android.email.provider.EmailContent.AccountColumns; import com.android.email.provider.EmailContent.AccountColumns;
import com.android.email.provider.EmailContent.HostAuth; import com.android.email.provider.EmailContent.HostAuth;
import com.android.email.provider.EmailContent.Mailbox; import com.android.email.provider.EmailContent.Mailbox;
import com.android.email.provider.EmailContent.Message;
import com.android.email.provider.EmailProvider; import com.android.email.provider.EmailProvider;
import android.accounts.AccountManager; import android.accounts.AccountManager;
@ -44,11 +48,12 @@ import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.ContentUris; 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.content.SyncStatusObserver; import android.content.SyncStatusObserver;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.AudioManager; import android.media.AudioManager;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.net.Uri; import android.net.Uri;
@ -56,6 +61,7 @@ import android.os.Bundle;
import android.os.Handler; 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.util.Config; import android.util.Config;
import android.util.Log; import android.util.Log;
@ -73,11 +79,6 @@ public class MailService extends Service {
private static final String LOG_TAG = "Email-MailService"; 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 = private static final String ACTION_CHECK_MAIL =
"com.android.email.intent.action.MAIL_SERVICE_WAKEUP"; "com.android.email.intent.action.MAIL_SERVICE_WAKEUP";
private static final String ACTION_RESCHEDULE = private static final String ACTION_RESCHEDULE =
@ -149,7 +150,7 @@ public class MailService extends Service {
synchronized (mSyncReports) { synchronized (mSyncReports) {
for (AccountSyncReport report : mSyncReports.values()) { for (AccountSyncReport report : mSyncReports.values()) {
if (accountId == -1 || accountId == report.accountId) { if (accountId == -1 || accountId == report.accountId) {
report.numNewMessages = 0; report.unseenMessageCount = 0;
} }
} }
} }
@ -182,12 +183,6 @@ public class MailService extends Service {
context.startService(i); 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 @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId); super.onStartCommand(intent, flags, startId);
@ -282,7 +277,7 @@ public class MailService extends Service {
// As a precaution, clear any outstanding Email notifications // As a precaution, clear any outstanding Email notifications
// We could be smarter and only do this when the list of accounts changes, // 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. // 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 // 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
@ -496,22 +491,34 @@ public class MailService extends Service {
long accountId; long accountId;
long prevSyncTime; // 0 == unknown long prevSyncTime; // 0 == unknown
long nextSyncTime; // 0 == ASAP -1 == don't sync 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; int syncInterval;
boolean notify; boolean notify;
boolean vibrate;
boolean vibrateWhenSilent;
Uri ringtoneUri;
String accountDisplayName;
boolean syncEnabled; // whether auto sync is enabled for this account 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 accountDisplayName + ": id=" + accountId + " prevSync=" + prevSyncTime return "id=" + accountId
+ " nextSync=" + nextSyncTime + " numNew=" + numNewMessages; + " 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 syncInterval = c.getInt(Account.CONTENT_SYNC_INTERVAL_COLUMN);
int flags = c.getInt(Account.CONTENT_FLAGS_COLUMN); int flags = c.getInt(Account.CONTENT_FLAGS_COLUMN);
long id = c.getInt(Account.CONTENT_ID_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 we're not using MessagingController (EAS at this point), don't schedule syncs
if (!mController.isMessagingController(id)) { if (!mController.isMessagingController(id)) {
@ -572,16 +578,10 @@ public class MailService extends Service {
report.accountId = c.getLong(Account.CONTENT_ID_COLUMN); report.accountId = c.getLong(Account.CONTENT_ID_COLUMN);
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.numNewMessages = 0; report.unseenMessageCount = 0;
report.syncInterval = syncInterval; report.syncInterval = syncInterval;
report.notify = (flags & Account.FLAGS_NOTIFY_NEW_MAIL) != 0; 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 // See if the account is enabled for sync in AccountManager
Account providerAccount = Account.restoreAccountWithId(this, id); Account providerAccount = Account.restoreAccountWithId(this, id);
@ -623,7 +623,7 @@ public class MailService extends Service {
report.nextSyncTime = report.prevSyncTime + (report.syncInterval * 1000 * 60); report.nextSyncTime = report.prevSyncTime + (report.syncInterval * 1000 * 60);
} }
if (newCount != -1) { if (newCount != -1) {
report.numNewMessages = newCount; report.unseenMessageCount = newCount;
} }
if (Config.LOGD && Email.DEBUG) { if (Config.LOGD && Email.DEBUG) {
Log.d(LOG_TAG, "update account " + report.toString()); 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 * Show "new message" notification for an account. (Notification is shown per account.)
* 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.
*/ */
private void notifyNewMessages(long accountId) { private void notifyNewMessages(final long accountId) {
boolean notify = false; final int unseenMessageCount;
boolean vibrate = false; final int justFetchedCount;
boolean vibrateWhenSilent = false;
Uri ringtone = null;
int accountsWithNewMessages = 0;
int numNewMessages = 0;
String reportName = null;
synchronized (mSyncReports) { synchronized (mSyncReports) {
for (AccountSyncReport report : mSyncReports.values()) { AccountSyncReport report = mSyncReports.get(accountId);
if (report.numNewMessages == 0) { if (report == null || report.unseenMessageCount == 0 || !report.notify) {
continue; return;
}
numNewMessages += report.numNewMessages;
accountsWithNewMessages += 1;
if (report.accountId == accountId) {
notify = report.notify;
vibrate = report.vibrate;
vibrateWhenSilent = report.vibrateWhenSilent;
ringtone = report.ringtoneUri;
reportName = report.accountDisplayName;
}
} }
} unseenMessageCount = report.unseenMessageCount;
if (!notify) { justFetchedCount = report.getJustFetchedMessageCount();
return; report.lastUnseenMessageCount = report.unseenMessageCount;
} }
showNewMessageNotification(this, accountId, vibrate, vibrateWhenSilent, ringtone, NotificationController.getInstance(this).showNewMessageNotification(accountId,
accountsWithNewMessages, numNewMessages, reportName); unseenMessageCount, justFetchedCount);
}
/** 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);
} }
/** /**

View File

@ -17,8 +17,8 @@
package com.android.exchange; package com.android.exchange;
import com.android.email.Email; import com.android.email.Email;
import com.android.email.NotificationController;
import com.android.email.R; import com.android.email.R;
import com.android.email.service.MailService;
import android.accounts.Account; import android.accounts.Account;
import android.accounts.AccountManager; import android.accounts.AccountManager;
@ -30,7 +30,6 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.provider.Calendar; import android.provider.Calendar;
import android.provider.ContactsContract;
import android.util.Log; import android.util.Log;
/** /**
@ -106,7 +105,7 @@ public class CalendarSyncEnabler {
NotificationManager nm = NotificationManager nm =
(NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); (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. */ /** @return {@link Intent} to launch the Calendar app. */

View File

@ -2210,4 +2210,39 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
assertEquals(0, Account.restoreAccountWithId(c, a4.mId).mNewMessageCount); assertEquals(0, Account.restoreAccountWithId(c, a4.mId).mNewMessageCount);
assertEquals(0, Account.restoreAccountWithId(c, a5.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));
}
} }

View File

@ -18,7 +18,7 @@ package com.android.exchange;
import com.android.email.AccountTestCase; import com.android.email.AccountTestCase;
import com.android.email.Email; import com.android.email.Email;
import com.android.email.service.MailService; import com.android.email.NotificationController;
import android.accounts.Account; import android.accounts.Account;
import android.accounts.AccountManager; import android.accounts.AccountManager;
@ -193,7 +193,9 @@ public class CalendarSyncEnablerTest extends AccountTestCase {
enabler.showNotification("a@b.com"); enabler.showNotification("a@b.com");
// Remove the notification. Comment it out when you want to know how it looks like. // 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)) ((NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE))
.cancel(MailService.NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED); .cancel(NotificationController.NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED);
} }
} }