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); + } +}