diff --git a/res/values/strings.xml b/res/values/strings.xml
index 15d2b0944..3f43c2deb 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -33,9 +33,15 @@
-
+
-
+
+
+
+
+
+
+
@@ -180,48 +186,15 @@
- 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
-
+
+ to %1$s
- %1$d account
- %1$d accounts
-
Inbox
diff --git a/res/values/styles.xml b/res/values/styles.xml
index a26906f69..b91306b81 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -72,4 +72,8 @@
- 1dip
- @color/divider_color
+
+
diff --git a/src/com/android/email/NotificationController.java b/src/com/android/email/NotificationController.java
index 965f7a4dc..6d524b238 100644
--- a/src/com/android/email/NotificationController.java
+++ b/src/com/android/email/NotificationController.java
@@ -31,9 +31,12 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
import android.media.AudioManager;
import android.net.Uri;
+import android.text.SpannableString;
import android.text.TextUtils;
+import android.text.style.TextAppearanceSpan;
/**
* Class that manages notifications.
@@ -54,19 +57,23 @@ public class NotificationController {
private final Context mContext;
private final NotificationManager mNotificationManager;
private final AudioManager mAudioManager;
+ private final Bitmap mAppIcon;
+ private final Clock mClock;
/** Constructor */
- private NotificationController(Context context) {
+ /* package */ NotificationController(Context context, Clock clock) {
mContext = context.getApplicationContext();
mNotificationManager = (NotificationManager) context.getSystemService(
Context.NOTIFICATION_SERVICE);
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ mAppIcon = BitmapFactory.decodeResource(mContext.getResources(), R.mipmap.icon);
+ mClock = clock;
}
/** Singleton access */
public static synchronized NotificationController getInstance(Context context) {
if (sInstance == null) {
- sInstance = new NotificationController(context);
+ sInstance = new NotificationController(context, Clock.INSTANCE);
}
return sInstance;
}
@@ -159,8 +166,7 @@ public class NotificationController {
Utility.runAsync(new Runnable() {
@Override
public void run() {
- Notification n = createNewMessageNotification(accountId, unseenMessageCount,
- justFetchedCount);
+ Notification n = createNewMessageNotification(accountId, unseenMessageCount);
if (n == null) {
return;
}
@@ -190,11 +196,9 @@ public class NotificationController {
* 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) {
+ /* package */ Notification createNewMessageNotification(long accountId,
+ int unseenMessageCount) {
final Account account = Account.restoreAccountWithId(mContext, accountId);
if (account == null) {
return null;
@@ -214,35 +218,16 @@ public class NotificationController {
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.Builder builder = new Notification.Builder(mContext)
.setSmallIcon(R.drawable.stat_notify_email_generic)
- .setWhen(System.currentTimeMillis())
- .setTicker(mContext.getString(R.string.notification_new_title))
- .setLargeIcon(senderPhoto)
- .setContentTitle(notificationTitle)
- .setContentText(subject + "\n" + numNewMessages)
+ .setWhen(mClock.getTime())
+ .setLargeIcon(senderPhoto != null ? senderPhoto : mAppIcon)
+ .setContentTitle(getNotificationTitle(senderName, account.mDisplayName))
+ .setContentText(subject)
.setContentIntent(contentIntent);
+ if (unseenMessageCount > 1) {
+ builder.setNumber(unseenMessageCount);
+ }
Notification notification = builder.getNotification();
@@ -250,11 +235,44 @@ public class NotificationController {
return notification;
}
- private boolean isRingerModeSilent() {
- return mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL;
+ /**
+ * Creates the notification title.
+ *
+ * If only 1 account, just show the sender name.
+ * If 2+ accounts, make it "SENDER_NAME to RECEIVER_NAME", and gray out the "to RECEIVER_NAME"
+ * part.
+ */
+ /* package */ SpannableString getNotificationTitle(String sender, String receiverDisplayName) {
+ final int numAccounts = EmailContent.count(mContext, Account.CONTENT_URI);
+ if (numAccounts == 1) {
+ return new SpannableString(sender);
+ } else {
+ // "to [account name]"
+ String toAcccount = mContext.getResources().getString(R.string.notification_to_account,
+ receiverDisplayName);
+ // "[Sender] to [account name]"
+ SpannableString senderToAccount = new SpannableString(sender + " " + toAcccount);
+
+ // "[Sender] to [account name]"
+ // ^^^^^^^^^^^^^^^^^ <- Make this part gray
+ TextAppearanceSpan secondarySpan = new TextAppearanceSpan(
+ mContext, R.style.notification_secondary_text);
+ senderToAccount.setSpan(secondarySpan, sender.length() + 1, senderToAccount.length(),
+ 0);
+ return senderToAccount;
+ }
}
- private void setupNotificationSoundAndVibrationFromAccount(Notification notification,
+ // Overridden for testing (AudioManager can't be mocked out.)
+ /* package */ int getRingerMode() {
+ return mAudioManager.getRingerMode();
+ }
+
+ /* package */ boolean isRingerModeSilent() {
+ return getRingerMode() != AudioManager.RINGER_MODE_NORMAL;
+ }
+
+ /* package */ void setupNotificationSoundAndVibrationFromAccount(Notification notification,
Account account) {
final int flags = account.mFlags;
final String ringtoneUri = account.mRingtoneUri;
@@ -283,7 +301,7 @@ public class NotificationController {
PendingIntent.FLAG_UPDATE_CURRENT);
}
Notification n = new Notification(android.R.drawable.stat_notify_error, tickerText,
- System.currentTimeMillis());
+ mClock.getTime());
n.setLatestEventInfo(mContext, tickerText, notificationText, pendingIntent);
n.flags = Notification.FLAG_AUTO_CANCEL;
mNotificationManager.notify(id, n);
diff --git a/tests/src/com/android/email/DBTestHelper.java b/tests/src/com/android/email/DBTestHelper.java
index feaea48d8..b83699f6e 100644
--- a/tests/src/com/android/email/DBTestHelper.java
+++ b/tests/src/com/android/email/DBTestHelper.java
@@ -25,6 +25,7 @@ import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.res.Resources;
+import android.content.res.Resources.Theme;
import android.database.Cursor;
import android.net.Uri;
import android.test.IsolatedContext;
@@ -175,14 +176,30 @@ public final class DBTestHelper {
/** {@link IsolatedContext} + getApplicationContext() */
private static class MyIsolatedContext extends IsolatedContext {
- public MyIsolatedContext(ContentResolver resolver, Context targetContext) {
+ private final Context mRealContext;
+
+ public MyIsolatedContext(ContentResolver resolver, Context targetContext,
+ Context realContext) {
super(resolver, targetContext);
+ mRealContext = realContext;
}
@Override
public Context getApplicationContext() {
return this;
}
+
+ // Following methods are not supported by the mock context.
+ // Redirect to the actual context.
+ @Override
+ public String getPackageName() {
+ return mRealContext.getPackageName();
+ }
+
+ @Override
+ public Theme getTheme() {
+ return mRealContext.getTheme();
+ }
}
// Based on ProviderTestCase2.setUp().
@@ -193,7 +210,8 @@ public final class DBTestHelper {
new MockContext2(context), // The context that most methods are delegated to
context, // The context that file methods are delegated to
filenamePrefix);
- final Context providerContext = new MyIsolatedContext(resolver, targetContextWrapper);
+ final Context providerContext = new MyIsolatedContext(resolver, targetContextWrapper,
+ context);
providerContext.getContentResolver();
// register EmailProvider and AttachmentProvider.
diff --git a/tests/src/com/android/email/NotificationControllerTest.java b/tests/src/com/android/email/NotificationControllerTest.java
new file mode 100644
index 000000000..2e404b246
--- /dev/null
+++ b/tests/src/com/android/email/NotificationControllerTest.java
@@ -0,0 +1,244 @@
+/*
+ * 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.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.email.provider.EmailContent.Message;
+import com.android.email.provider.ProviderTestUtils;
+
+import android.app.Notification;
+import android.content.Context;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.test.AndroidTestCase;
+
+/**
+ * Test for {@link NotificationController}.
+ *
+ * TODO Add tests for all methods.
+ */
+public class NotificationControllerTest extends AndroidTestCase {
+ private Context mProviderContext;
+ private NotificationController mTarget;
+
+ private final MockClock mMockClock = new MockClock();
+ private int mRingerMode;
+
+ /**
+ * Subclass {@link NotificationController} to override un-mockable operations.
+ */
+ private class NotificationControllerForTest extends NotificationController {
+ NotificationControllerForTest(Context context) {
+ super(context, mMockClock);
+ }
+
+ @Override
+ int getRingerMode() {
+ return mRingerMode;
+ }
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mProviderContext = DBTestHelper.ProviderContextSetupHelper.getProviderContext(mContext);
+ mTarget = new NotificationControllerForTest(mProviderContext);
+ }
+
+ public void testSetupNotificationSoundAndVibrationFromAccount() {
+ final Notification n = new Notification();
+
+ final Context c = mProviderContext;
+ final Account a1 = ProviderTestUtils.setupAccount("a1", true, c);
+
+ // === Ringer mode change ===
+ mRingerMode = AudioManager.RINGER_MODE_NORMAL;
+
+ // VIBRATE_ALWAYS, with a ringer tone
+ a1.mFlags = Account.FLAGS_VIBRATE_ALWAYS;
+
+ n.defaults = 0;
+ n.flags = 0;
+ mTarget.setupNotificationSoundAndVibrationFromAccount(n, a1);
+
+ assertEquals(Uri.parse(a1.mRingtoneUri), n.sound);
+ assertTrue((n.defaults & Notification.DEFAULT_VIBRATE) != 0);
+ assertTrue((n.flags & Notification.FLAG_SHOW_LIGHTS) != 0); // always set
+ assertTrue((n.defaults & Notification.DEFAULT_LIGHTS) != 0); // always set
+
+ // FLAGS_VIBRATE_WHEN_SILENT, with a ringer tone
+ a1.mFlags = Account.FLAGS_VIBRATE_WHEN_SILENT;
+
+ n.defaults = 0;
+ n.flags = 0;
+ mTarget.setupNotificationSoundAndVibrationFromAccount(n, a1);
+
+ assertEquals(Uri.parse(a1.mRingtoneUri), n.sound);
+ assertFalse((n.defaults & Notification.DEFAULT_VIBRATE) != 0); // no vibe
+ assertTrue((n.flags & Notification.FLAG_SHOW_LIGHTS) != 0); // always set
+ assertTrue((n.defaults & Notification.DEFAULT_LIGHTS) != 0); // always set
+
+ // No VIBRATE flags, with a ringer tone
+ a1.mFlags = 0;
+
+ n.defaults = 0;
+ n.flags = 0;
+ mTarget.setupNotificationSoundAndVibrationFromAccount(n, a1);
+
+ assertEquals(Uri.parse(a1.mRingtoneUri), n.sound);
+ assertFalse((n.defaults & Notification.DEFAULT_VIBRATE) != 0); // no vibe
+ assertTrue((n.flags & Notification.FLAG_SHOW_LIGHTS) != 0); // always set
+ assertTrue((n.defaults & Notification.DEFAULT_LIGHTS) != 0); // always set
+
+ // === Ringer mode change ===
+ mRingerMode = AudioManager.RINGER_MODE_VIBRATE;
+
+ // VIBRATE_ALWAYS, with a ringer tone
+ a1.mFlags = Account.FLAGS_VIBRATE_ALWAYS;
+
+ n.defaults = 0;
+ n.flags = 0;
+ mTarget.setupNotificationSoundAndVibrationFromAccount(n, a1);
+
+ assertEquals(Uri.parse(a1.mRingtoneUri), n.sound);
+ assertTrue((n.defaults & Notification.DEFAULT_VIBRATE) != 0);
+ assertTrue((n.flags & Notification.FLAG_SHOW_LIGHTS) != 0); // always set
+ assertTrue((n.defaults & Notification.DEFAULT_LIGHTS) != 0); // always set
+
+ // FLAGS_VIBRATE_WHEN_SILENT, with a ringer tone
+ a1.mFlags = Account.FLAGS_VIBRATE_WHEN_SILENT;
+
+ n.defaults = 0;
+ n.flags = 0;
+ mTarget.setupNotificationSoundAndVibrationFromAccount(n, a1);
+
+ assertEquals(Uri.parse(a1.mRingtoneUri), n.sound);
+ assertTrue((n.defaults & Notification.DEFAULT_VIBRATE) != 0);
+ assertTrue((n.flags & Notification.FLAG_SHOW_LIGHTS) != 0); // always set
+ assertTrue((n.defaults & Notification.DEFAULT_LIGHTS) != 0); // always set
+
+ // No VIBRATE flags, with a ringer tone
+ a1.mFlags = 0;
+
+ n.defaults = 0;
+ n.flags = 0;
+ mTarget.setupNotificationSoundAndVibrationFromAccount(n, a1);
+
+ assertEquals(Uri.parse(a1.mRingtoneUri), n.sound);
+ assertFalse((n.defaults & Notification.DEFAULT_VIBRATE) != 0); // no vibe
+ assertTrue((n.flags & Notification.FLAG_SHOW_LIGHTS) != 0); // always set
+ assertTrue((n.defaults & Notification.DEFAULT_LIGHTS) != 0); // always set
+
+ // === Ringer mode change ===
+ mRingerMode = AudioManager.RINGER_MODE_SILENT;
+
+ // VIBRATE_ALWAYS, with a ringer tone
+ a1.mFlags = Account.FLAGS_VIBRATE_ALWAYS;
+
+ n.defaults = 0;
+ n.flags = 0;
+ mTarget.setupNotificationSoundAndVibrationFromAccount(n, a1);
+
+ assertEquals(Uri.parse(a1.mRingtoneUri), n.sound);
+ assertTrue((n.defaults & Notification.DEFAULT_VIBRATE) != 0);
+ assertTrue((n.flags & Notification.FLAG_SHOW_LIGHTS) != 0); // always set
+ assertTrue((n.defaults & Notification.DEFAULT_LIGHTS) != 0); // always set
+
+ // FLAGS_VIBRATE_WHEN_SILENT, with a ringer tone
+ a1.mFlags = Account.FLAGS_VIBRATE_WHEN_SILENT;
+
+ n.defaults = 0;
+ n.flags = 0;
+ mTarget.setupNotificationSoundAndVibrationFromAccount(n, a1);
+
+ assertEquals(Uri.parse(a1.mRingtoneUri), n.sound);
+ assertTrue((n.defaults & Notification.DEFAULT_VIBRATE) != 0);
+ assertTrue((n.flags & Notification.FLAG_SHOW_LIGHTS) != 0); // always set
+ assertTrue((n.defaults & Notification.DEFAULT_LIGHTS) != 0); // always set
+
+ // No VIBRATE flags, with a ringer tone
+ a1.mFlags = 0;
+
+ n.defaults = 0;
+ n.flags = 0;
+ mTarget.setupNotificationSoundAndVibrationFromAccount(n, a1);
+
+ assertEquals(Uri.parse(a1.mRingtoneUri), n.sound);
+ assertFalse((n.defaults & Notification.DEFAULT_VIBRATE) != 0); // no vibe
+ assertTrue((n.flags & Notification.FLAG_SHOW_LIGHTS) != 0); // always set
+ assertTrue((n.defaults & Notification.DEFAULT_LIGHTS) != 0); // always set
+
+ // No ringer tone
+ a1.mRingtoneUri = null;
+
+ n.defaults = 0;
+ n.flags = 0;
+ mTarget.setupNotificationSoundAndVibrationFromAccount(n, a1);
+
+ assertNull(n.sound);
+ }
+
+ public void testCreateNewMessageNotification() {
+ final Context c = mProviderContext;
+ Notification n;
+
+ // Case 1: 1 account, 1 unseen message
+ Account a1 = ProviderTestUtils.setupAccount("a1", true, c);
+ Mailbox b1 = ProviderTestUtils.setupMailbox("inbox", a1.mId, true, c, Mailbox.TYPE_INBOX);
+ Message m1 = ProviderTestUtils.setupMessage("message", a1.mId, b1.mId, true, true, c);
+
+ n = mTarget.createNewMessageNotification(a1.mId, 1);
+
+ assertEquals(R.drawable.stat_notify_email_generic, n.icon);
+ assertEquals(mMockClock.mTime, n.when);
+ assertNotNull(n.largeIcon);
+ assertEquals(0, n.number);
+
+ // TODO Check content -- how?
+
+ // Case 2: 1 account, 2 unseen message
+ n = mTarget.createNewMessageNotification(a1.mId, 2);
+
+ assertEquals(R.drawable.stat_notify_email_generic, n.icon);
+ assertEquals(mMockClock.mTime, n.when);
+ assertNotNull(n.largeIcon);
+ assertEquals(2, n.number);
+
+ // TODO Check content -- how?
+
+ // TODO Add 2 account test, if we find a way to check content
+ }
+
+ public void testGetNotificationTitle() {
+ final Context c = mProviderContext;
+
+ // Case 1: 1 account
+ Account a1 = ProviderTestUtils.setupAccount("a1", true, c);
+
+ // Just check the content. Ignore the spans.
+ String title = mTarget.getNotificationTitle("*sender*", "*receiver*").toString();
+ assertEquals("*sender*", title);
+
+ // Case 1: 2 account
+ Account a2 = ProviderTestUtils.setupAccount("a1", true, c);
+
+ // Just check the content. Ignore the spans.
+ title = mTarget.getNotificationTitle("*sender*", "*receiver*").toString();
+ assertEquals("*sender* to *receiver*", title);
+ }
+}