NotificationUtils.java revision c94c80c8b8c92c019f19f2e315dea1ff03ad573f
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.mail.utils;
17
18import android.app.Notification;
19import android.app.NotificationManager;
20import android.app.PendingIntent;
21import android.content.ContentResolver;
22import android.content.ContentUris;
23import android.content.ContentValues;
24import android.content.Context;
25import android.content.Intent;
26import android.content.res.Resources;
27import android.database.Cursor;
28import android.graphics.Bitmap;
29import android.graphics.BitmapFactory;
30import android.net.Uri;
31import android.provider.ContactsContract;
32import android.provider.ContactsContract.CommonDataKinds.Email;
33import android.provider.ContactsContract.Contacts.Photo;
34import android.support.v4.app.NotificationCompat;
35import android.support.v4.text.BidiFormatter;
36import android.text.SpannableString;
37import android.text.SpannableStringBuilder;
38import android.text.TextUtils;
39import android.text.style.CharacterStyle;
40import android.text.style.TextAppearanceSpan;
41import android.util.Pair;
42import android.util.SparseArray;
43
44import com.android.mail.EmailAddress;
45import com.android.mail.MailIntentService;
46import com.android.mail.R;
47import com.android.mail.browse.MessageCursor;
48import com.android.mail.browse.SendersView;
49import com.android.mail.photomanager.ContactPhotoManager;
50import com.android.mail.preferences.AccountPreferences;
51import com.android.mail.preferences.FolderPreferences;
52import com.android.mail.preferences.MailPrefs;
53import com.android.mail.providers.Account;
54import com.android.mail.providers.Address;
55import com.android.mail.providers.Conversation;
56import com.android.mail.providers.Folder;
57import com.android.mail.providers.Message;
58import com.android.mail.providers.UIProvider;
59import com.android.mail.ui.ImageCanvas.Dimensions;
60import com.android.mail.utils.NotificationActionUtils.NotificationAction;
61import com.google.android.common.html.parser.HTML;
62import com.google.android.common.html.parser.HTML4;
63import com.google.android.common.html.parser.HtmlDocument;
64import com.google.android.common.html.parser.HtmlTree;
65import com.google.common.base.Objects;
66import com.google.common.collect.ImmutableList;
67import com.google.common.collect.Lists;
68import com.google.common.collect.Sets;
69
70import java.io.ByteArrayInputStream;
71import java.util.ArrayList;
72import java.util.Arrays;
73import java.util.Collection;
74import java.util.List;
75import java.util.Set;
76import java.util.concurrent.ConcurrentHashMap;
77
78public class NotificationUtils {
79    public static final String LOG_TAG = LogTag.getLogTag();
80
81    /** Contains a list of <(account, label), unread conversations> */
82    private static NotificationMap sActiveNotificationMap = null;
83
84    private static final SparseArray<Bitmap> sNotificationIcons = new SparseArray<Bitmap>();
85
86    private static TextAppearanceSpan sNotificationUnreadStyleSpan;
87    private static CharacterStyle sNotificationReadStyleSpan;
88
89    /** A factory that produces a plain text converter that removes elided text. */
90    private static final HtmlTree.PlainTextConverterFactory MESSAGE_CONVERTER_FACTORY =
91            new HtmlTree.PlainTextConverterFactory() {
92                @Override
93                public HtmlTree.PlainTextConverter createInstance() {
94                    return new MailMessagePlainTextConverter();
95                }
96            };
97
98    private static final BidiFormatter sBidiFormatter = BidiFormatter.getInstance();
99
100    /**
101     * Clears all notifications in response to the user tapping "Clear" in the status bar.
102     */
103    public static void clearAllNotfications(Context context) {
104        LogUtils.v(LOG_TAG, "NotificationUtils: Clearing all notifications.");
105        final NotificationMap notificationMap = getNotificationMap(context);
106        notificationMap.clear();
107        notificationMap.saveNotificationMap(context);
108    }
109
110    /**
111     * Returns the notification map, creating it if necessary.
112     */
113    private static synchronized NotificationMap getNotificationMap(Context context) {
114        if (sActiveNotificationMap == null) {
115            sActiveNotificationMap = new NotificationMap();
116
117            // populate the map from the cached data
118            sActiveNotificationMap.loadNotificationMap(context);
119        }
120        return sActiveNotificationMap;
121    }
122
123    /**
124     * Class representing the existing notifications, and the number of unread and
125     * unseen conversations that triggered each.
126     */
127    private static class NotificationMap
128            extends ConcurrentHashMap<NotificationKey, Pair<Integer, Integer>> {
129
130        private static final String NOTIFICATION_PART_SEPARATOR = " ";
131        private static final int NUM_NOTIFICATION_PARTS= 4;
132
133        /**
134         * Retuns the unread count for the given NotificationKey.
135         */
136        public Integer getUnread(NotificationKey key) {
137            final Pair<Integer, Integer> value = get(key);
138            return value != null ? value.first : null;
139        }
140
141        /**
142         * Retuns the unread unseen count for the given NotificationKey.
143         */
144        public Integer getUnseen(NotificationKey key) {
145            final Pair<Integer, Integer> value = get(key);
146            return value != null ? value.second : null;
147        }
148
149        /**
150         * Store the unread and unseen value for the given NotificationKey
151         */
152        public void put(NotificationKey key, int unread, int unseen) {
153            final Pair<Integer, Integer> value =
154                    new Pair<Integer, Integer>(Integer.valueOf(unread), Integer.valueOf(unseen));
155            put(key, value);
156        }
157
158        /**
159         * Populates the notification map with previously cached data.
160         */
161        public synchronized void loadNotificationMap(final Context context) {
162            final MailPrefs mailPrefs = MailPrefs.get(context);
163            final Set<String> notificationSet = mailPrefs.getActiveNotificationSet();
164            if (notificationSet != null) {
165                for (String notificationEntry : notificationSet) {
166                    // Get the parts of the string that make the notification entry
167                    final String[] notificationParts =
168                            TextUtils.split(notificationEntry, NOTIFICATION_PART_SEPARATOR);
169                    if (notificationParts.length == NUM_NOTIFICATION_PARTS) {
170                        final Uri accountUri = Uri.parse(notificationParts[0]);
171                        final Cursor accountCursor = context.getContentResolver().query(
172                                accountUri, UIProvider.ACCOUNTS_PROJECTION, null, null, null);
173                        final Account account;
174                        try {
175                            if (accountCursor.moveToFirst()) {
176                                account = new Account(accountCursor);
177                            } else {
178                                continue;
179                            }
180                        } finally {
181                            accountCursor.close();
182                        }
183
184                        final Uri folderUri = Uri.parse(notificationParts[1]);
185                        final Cursor folderCursor = context.getContentResolver().query(
186                                folderUri, UIProvider.FOLDERS_PROJECTION, null, null, null);
187                        final Folder folder;
188                        try {
189                            if (folderCursor.moveToFirst()) {
190                                folder = new Folder(folderCursor);
191                            } else {
192                                continue;
193                            }
194                        } finally {
195                            folderCursor.close();
196                        }
197
198                        final NotificationKey key = new NotificationKey(account, folder);
199                        final Integer unreadValue = Integer.valueOf(notificationParts[2]);
200                        final Integer unseenValue = Integer.valueOf(notificationParts[3]);
201                        final Pair<Integer, Integer> unreadUnseenValue =
202                                new Pair<Integer, Integer>(unreadValue, unseenValue);
203                        put(key, unreadUnseenValue);
204                    }
205                }
206            }
207        }
208
209        /**
210         * Cache the notification map.
211         */
212        public synchronized void saveNotificationMap(Context context) {
213            final Set<String> notificationSet = Sets.newHashSet();
214            final Set<NotificationKey> keys = keySet();
215            for (NotificationKey key : keys) {
216                final Pair<Integer, Integer> value = get(key);
217                final Integer unreadCount = value.first;
218                final Integer unseenCount = value.second;
219                if (unreadCount != null && unseenCount != null) {
220                    final String[] partValues = new String[] {
221                            key.account.uri.toString(), key.folder.uri.toString(),
222                            unreadCount.toString(), unseenCount.toString()};
223                    notificationSet.add(TextUtils.join(NOTIFICATION_PART_SEPARATOR, partValues));
224                }
225            }
226            final MailPrefs mailPrefs = MailPrefs.get(context);
227            mailPrefs.cacheActiveNotificationSet(notificationSet);
228        }
229    }
230
231    /**
232     * @return the title of this notification with each account and the number of unread and unseen
233     * conversations for it. Also remove any account in the map that has 0 unread.
234     */
235    private static String createNotificationString(NotificationMap notifications) {
236        StringBuilder result = new StringBuilder();
237        int i = 0;
238        Set<NotificationKey> keysToRemove = Sets.newHashSet();
239        for (NotificationKey key : notifications.keySet()) {
240            Integer unread = notifications.getUnread(key);
241            Integer unseen = notifications.getUnseen(key);
242            if (unread == null || unread.intValue() == 0) {
243                keysToRemove.add(key);
244            } else {
245                if (i > 0) result.append(", ");
246                result.append(key.toString() + " (" + unread + ", " + unseen + ")");
247                i++;
248            }
249        }
250
251        for (NotificationKey key : keysToRemove) {
252            notifications.remove(key);
253        }
254
255        return result.toString();
256    }
257
258    /**
259     * Get all notifications for all accounts and cancel them.
260     **/
261    public static void cancelAllNotifications(Context context) {
262        LogUtils.d(LOG_TAG, "NotificationUtils: cancelAllNotifications - cancelling all");
263        NotificationManager nm = (NotificationManager) context.getSystemService(
264                Context.NOTIFICATION_SERVICE);
265        nm.cancelAll();
266        clearAllNotfications(context);
267    }
268
269    /**
270     * Get all notifications for all accounts, cancel them, and repost.
271     * This happens when locale changes.
272     **/
273    public static void cancelAndResendNotifications(Context context) {
274        LogUtils.d(LOG_TAG, "NotificationUtils: cancelAndResendNotifications");
275        resendNotifications(context, true, null, null);
276    }
277
278    /**
279     * Get all notifications for all accounts, optionally cancel them, and repost.
280     * This happens when locale changes. If you only want to resend messages from one
281     * account-folder pair, pass in the account and folder that should be resent.
282     * All other account-folder pairs will not have their notifications resent.
283     * All notifications will be resent if account or folder is null.
284     *
285     * @param context Current context.
286     * @param cancelExisting True, if all notifications should be canceled before resending.
287     *                       False, otherwise.
288     * @param accountUri The {@link Uri} of the {@link Account} of the notification
289     *                   upon which an action occurred.
290     * @param folderUri The {@link Uri} of the {@link Folder} of the notification
291     *                  upon which an action occurred.
292     */
293    public static void resendNotifications(Context context, final boolean cancelExisting,
294            final Uri accountUri, final Uri folderUri) {
295        LogUtils.d(LOG_TAG, "NotificationUtils: resendNotifications ");
296
297        if (cancelExisting) {
298            LogUtils.d(LOG_TAG, "NotificationUtils: resendNotifications - cancelling all");
299            NotificationManager nm =
300                    (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
301            nm.cancelAll();
302        }
303        // Re-validate the notifications.
304        final NotificationMap notificationMap = getNotificationMap(context);
305        final Set<NotificationKey> keys = notificationMap.keySet();
306        for (NotificationKey notification : keys) {
307            final Folder folder = notification.folder;
308            final int notificationId = getNotificationId(notification.account.name,
309                    folder);
310
311            // Only resend notifications if the notifications are from the same folder
312            // and same account as the undo notification that was previously displayed.
313            if (accountUri != null && !Objects.equal(accountUri, notification.account.uri) &&
314                    folderUri != null && !Objects.equal(folderUri, folder.uri)) {
315                LogUtils.d(LOG_TAG, "NotificationUtils: resendNotifications - not resending %s / %s"
316                        + " because it doesn't match %s / %s",
317                        notification.account.uri, folder.uri, accountUri, folderUri);
318                continue;
319            }
320
321            LogUtils.d(LOG_TAG, "NotificationUtils: resendNotifications - resending %s / %s",
322                    notification.account.uri, folder.uri);
323
324            final NotificationAction undoableAction =
325                    NotificationActionUtils.sUndoNotifications.get(notificationId);
326            if (undoableAction == null) {
327                validateNotifications(context, folder, notification.account, true,
328                        false, notification);
329            } else {
330                // Create an undo notification
331                NotificationActionUtils.createUndoNotification(context, undoableAction);
332            }
333        }
334    }
335
336    /**
337     * Validate the notifications for the specified account.
338     */
339    public static void validateAccountNotifications(Context context, String account) {
340        LogUtils.d(LOG_TAG, "NotificationUtils: validateAccountNotifications - %s", account);
341
342        List<NotificationKey> notificationsToCancel = Lists.newArrayList();
343        // Iterate through the notification map to see if there are any entries that correspond to
344        // labels that are not in the sync set.
345        final NotificationMap notificationMap = getNotificationMap(context);
346        Set<NotificationKey> keys = notificationMap.keySet();
347        final AccountPreferences accountPreferences = new AccountPreferences(context, account);
348        final boolean enabled = accountPreferences.areNotificationsEnabled();
349        if (!enabled) {
350            // Cancel all notifications for this account
351            for (NotificationKey notification : keys) {
352                if (notification.account.name.equals(account)) {
353                    notificationsToCancel.add(notification);
354                }
355            }
356        } else {
357            // Iterate through the notification map to see if there are any entries that
358            // correspond to labels that are not in the notification set.
359            for (NotificationKey notification : keys) {
360                if (notification.account.name.equals(account)) {
361                    // If notification is not enabled for this label, remember this NotificationKey
362                    // to later cancel the notification, and remove the entry from the map
363                    final Folder folder = notification.folder;
364                    final boolean isInbox =
365                            notification.account.settings.defaultInbox.equals(folder.uri);
366                    final FolderPreferences folderPreferences = new FolderPreferences(
367                            context, notification.account.name, folder, isInbox);
368
369                    if (!folderPreferences.areNotificationsEnabled()) {
370                        notificationsToCancel.add(notification);
371                    }
372                }
373            }
374        }
375
376        // Cancel & remove the invalid notifications.
377        if (notificationsToCancel.size() > 0) {
378            NotificationManager nm = (NotificationManager) context.getSystemService(
379                    Context.NOTIFICATION_SERVICE);
380            for (NotificationKey notification : notificationsToCancel) {
381                final Folder folder = notification.folder;
382                final int notificationId = getNotificationId(notification.account.name, folder);
383                LogUtils.d(LOG_TAG,
384                        "NotificationUtils: validateAccountNotifications - cancelling %s / %s",
385                        notification.account.name, folder.persistentId);
386                nm.cancel(notificationId);
387                notificationMap.remove(notification);
388                NotificationActionUtils.sUndoNotifications.remove(notificationId);
389                NotificationActionUtils.sNotificationTimestamps.delete(notificationId);
390            }
391            notificationMap.saveNotificationMap(context);
392        }
393    }
394
395    /**
396     * Display only one notification.
397     */
398    public static void setNewEmailIndicator(Context context, final int unreadCount,
399            final int unseenCount, final Account account, final Folder folder,
400            final boolean getAttention) {
401        LogUtils.d(LOG_TAG, "NotificationUtils: setNewEmailIndicator unreadCount = %d, "
402            + "unseenCount = %d, account = %s, folder = %s, getAttention = %b", unreadCount,
403            unseenCount, account.name, folder.uri, getAttention);
404
405        boolean ignoreUnobtrusiveSetting = false;
406
407        final int notificationId = getNotificationId(account.name, folder);
408
409        // Update the notification map
410        final NotificationMap notificationMap = getNotificationMap(context);
411        final NotificationKey key = new NotificationKey(account, folder);
412        if (unreadCount == 0) {
413            LogUtils.d(LOG_TAG,
414                    "NotificationUtils: setNewEmailIndicator - cancelling %s / %s",
415                    account.name, folder.persistentId);
416            notificationMap.remove(key);
417            ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
418                    .cancel(notificationId);
419        } else {
420            if (!notificationMap.containsKey(key)) {
421                // This account previously didn't have any unread mail; ignore the "unobtrusive
422                // notifications" setting and play sound and/or vibrate the device even if a
423                // notification already exists (bug 2412348).
424                ignoreUnobtrusiveSetting = true;
425            }
426            notificationMap.put(key, unreadCount, unseenCount);
427        }
428        notificationMap.saveNotificationMap(context);
429
430        if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
431            LogUtils.v(LOG_TAG, "NotificationUtils: New email: %s mapSize: %d getAttention: %b",
432                    createNotificationString(notificationMap), notificationMap.size(),
433                    getAttention);
434        }
435
436        if (NotificationActionUtils.sUndoNotifications.get(notificationId) == null) {
437            validateNotifications(context, folder, account, getAttention, ignoreUnobtrusiveSetting,
438                    key);
439        }
440    }
441
442    /**
443     * Validate the notifications notification.
444     */
445    private static void validateNotifications(Context context, final Folder folder,
446            final Account account, boolean getAttention, boolean ignoreUnobtrusiveSetting,
447            NotificationKey key) {
448
449        NotificationManager nm = (NotificationManager)
450                context.getSystemService(Context.NOTIFICATION_SERVICE);
451
452        final NotificationMap notificationMap = getNotificationMap(context);
453        if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
454            LogUtils.v(LOG_TAG, "NotificationUtils: Validating Notification: %s mapSize: %d "
455                    + "folder: %s getAttention: %b", createNotificationString(notificationMap),
456                    notificationMap.size(), folder.name, getAttention);
457        }
458        // The number of unread messages for this account and label.
459        final Integer unread = notificationMap.getUnread(key);
460        final int unreadCount = unread != null ? unread.intValue() : 0;
461        final Integer unseen = notificationMap.getUnseen(key);
462        int unseenCount = unseen != null ? unseen.intValue() : 0;
463
464        Cursor cursor = null;
465
466        try {
467            final Uri.Builder uriBuilder = folder.conversationListUri.buildUpon();
468            uriBuilder.appendQueryParameter(
469                    UIProvider.SEEN_QUERY_PARAMETER, Boolean.FALSE.toString());
470            // Do not allow this quick check to disrupt any active network-enabled conversation
471            // cursor.
472            uriBuilder.appendQueryParameter(
473                    UIProvider.ConversationListQueryParameters.USE_NETWORK,
474                    Boolean.FALSE.toString());
475            cursor = context.getContentResolver().query(uriBuilder.build(),
476                    UIProvider.CONVERSATION_PROJECTION, null, null, null);
477            if (cursor == null) {
478                // This folder doesn't exist.
479                clearFolderNotification(context, account, folder, false);
480                return;
481            }
482            final int cursorUnseenCount = cursor.getCount();
483
484            // Make sure the unseen count matches the number of items in the cursor.  But, we don't
485            // want to overwrite a 0 unseen count that was specified in the intent
486            if (unseenCount != 0 && unseenCount != cursorUnseenCount) {
487                LogUtils.d(LOG_TAG, "NotificationUtils: "
488                        + "Unseen count doesn't match cursor count.  unseen: %d cursor count: %d",
489                        unseenCount, cursorUnseenCount);
490                unseenCount = cursorUnseenCount;
491            }
492
493            // For the purpose of the notifications, the unseen count should be capped at the num of
494            // unread conversations.
495            if (unseenCount > unreadCount) {
496                unseenCount = unreadCount;
497            }
498
499            final int notificationId = getNotificationId(account.name, folder);
500
501            if (unseenCount == 0) {
502                LogUtils.d(LOG_TAG,
503                        "NotificationUtils: validateNotifications - cancelling %s / %s",
504                        account.name, folder.persistentId);
505                nm.cancel(notificationId);
506                return;
507            }
508
509            // We now have all we need to create the notification and the pending intent
510            PendingIntent clickIntent;
511
512            NotificationCompat.Builder notification = new NotificationCompat.Builder(context);
513            notification.setSmallIcon(R.drawable.stat_notify_email);
514            notification.setTicker(account.name);
515
516            final long when;
517
518            final long oldWhen =
519                    NotificationActionUtils.sNotificationTimestamps.get(notificationId);
520            if (oldWhen != 0) {
521                when = oldWhen;
522            } else {
523                when = System.currentTimeMillis();
524            }
525
526            notification.setWhen(when);
527
528            // The timestamp is now stored in the notification, so we can remove it from here
529            NotificationActionUtils.sNotificationTimestamps.delete(notificationId);
530
531            // Dispatch a CLEAR_NEW_MAIL_NOTIFICATIONS intent if the user taps the "X" next to a
532            // notification.  Also this intent gets fired when the user taps on a notification as
533            // the AutoCancel flag has been set
534            final Intent cancelNotificationIntent =
535                    new Intent(MailIntentService.ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS);
536            cancelNotificationIntent.setPackage(context.getPackageName());
537            cancelNotificationIntent.setData(Utils.appendVersionQueryParameter(context,
538                    folder.uri));
539            cancelNotificationIntent.putExtra(Utils.EXTRA_ACCOUNT, account);
540            cancelNotificationIntent.putExtra(Utils.EXTRA_FOLDER, folder);
541
542            notification.setDeleteIntent(PendingIntent.getService(
543                    context, notificationId, cancelNotificationIntent, 0));
544
545            // Ensure that the notification is cleared when the user selects it
546            notification.setAutoCancel(true);
547
548            boolean eventInfoConfigured = false;
549
550            final boolean isInbox = account.settings.defaultInbox.equals(folder.uri);
551            final FolderPreferences folderPreferences =
552                    new FolderPreferences(context, account.name, folder, isInbox);
553
554            if (isInbox) {
555                final AccountPreferences accountPreferences =
556                        new AccountPreferences(context, account.name);
557                moveNotificationSetting(accountPreferences, folderPreferences);
558            }
559
560            if (!folderPreferences.areNotificationsEnabled()) {
561                // Don't notify
562                return;
563            }
564
565            if (unreadCount > 0) {
566                // How can I order this properly?
567                if (cursor.moveToNext()) {
568                    final Intent notificationIntent;
569
570                    // Launch directly to the conversation, if there is only 1 unseen conversation
571                    if (unseenCount == 1) {
572                        notificationIntent = createViewConversationIntent(context, account, folder,
573                                cursor);
574                    } else {
575                        notificationIntent = createViewConversationIntent(context, account, folder,
576                                null);
577                    }
578
579                    if (notificationIntent == null) {
580                        LogUtils.e(LOG_TAG, "NotificationUtils: "
581                                + "Null intent when building notification");
582                        return;
583                    }
584
585                    clickIntent = PendingIntent.getActivity(context, -1, notificationIntent, 0);
586                    configureLatestEventInfoFromConversation(context, account, folderPreferences,
587                            notification, cursor, clickIntent, notificationIntent,
588                            account.name, unreadCount, unseenCount, folder, when);
589                    eventInfoConfigured = true;
590                }
591            }
592
593            final boolean vibrate = folderPreferences.isNotificationVibrateEnabled();
594            final String ringtoneUri = folderPreferences.getNotificationRingtoneUri();
595            final boolean notifyOnce = !folderPreferences.isEveryMessageNotificationEnabled();
596
597            if (!ignoreUnobtrusiveSetting && notifyOnce) {
598                // If the user has "unobtrusive notifications" enabled, only alert the first time
599                // new mail is received in this account.  This is the default behavior.  See
600                // bugs 2412348 and 2413490.
601                notification.setOnlyAlertOnce(true);
602            }
603
604            LogUtils.d(LOG_TAG, "NotificationUtils: Account: %s vibrate: %s", account.name,
605                    Boolean.toString(folderPreferences.isNotificationVibrateEnabled()));
606
607            int defaults = 0;
608
609            /*
610             * We do not want to notify if this is coming back from an Undo notification, hence the
611             * oldWhen check.
612             */
613            if (getAttention && oldWhen == 0) {
614                final AccountPreferences accountPreferences =
615                        new AccountPreferences(context, account.name);
616                if (accountPreferences.areNotificationsEnabled()) {
617                    if (vibrate) {
618                        defaults |= Notification.DEFAULT_VIBRATE;
619                    }
620
621                    notification.setSound(TextUtils.isEmpty(ringtoneUri) ? null
622                            : Uri.parse(ringtoneUri));
623                    LogUtils.d(LOG_TAG, "NotificationUtils: "
624                            + "New email in %s vibrateWhen: %s, playing notification: %s",
625                            account.name, vibrate, ringtoneUri);
626                }
627            }
628
629            if (eventInfoConfigured) {
630                defaults |= Notification.DEFAULT_LIGHTS;
631                notification.setDefaults(defaults);
632
633                if (oldWhen != 0) {
634                    // We do not want to display the ticker again if we are re-displaying this
635                    // notification (like from an Undo notification)
636                    notification.setTicker(null);
637                }
638
639                nm.notify(notificationId, notification.build());
640            }
641        } finally {
642            if (cursor != null) {
643                cursor.close();
644            }
645        }
646    }
647
648    /**
649     * @return an {@link Intent} which, if launched, will display the corresponding conversation
650     */
651    private static Intent createViewConversationIntent(final Context context, final Account account,
652            final Folder folder, final Cursor cursor) {
653        if (folder == null || account == null) {
654            LogUtils.e(LOG_TAG, "NotificationUtils#createViewConversationIntent(): "
655                    + "Null account or folder.  account: %s folder: %s", account, folder);
656            return null;
657        }
658
659        final Intent intent;
660
661        if (cursor == null) {
662            intent = Utils.createViewFolderIntent(context, folder.uri, account);
663        } else {
664            // A conversation cursor has been specified, so this intent is intended to be go
665            // directly to the one new conversation
666
667            // Get the Conversation object
668            final Conversation conversation = new Conversation(cursor);
669            intent = Utils.createViewConversationIntent(context, conversation, folder.uri,
670                    account);
671        }
672
673        return intent;
674    }
675
676    private static Bitmap getDefaultNotificationIcon(
677            final Context context, final Folder folder, final boolean multipleNew) {
678        final int resId;
679        if (folder.notificationIconResId != 0) {
680            resId = folder.notificationIconResId;
681        } else if (multipleNew) {
682            resId = R.drawable.ic_notification_multiple_mail_holo_dark;
683        } else {
684            resId = R.drawable.ic_contact_picture;
685        }
686
687        final Bitmap icon = getIcon(context, resId);
688
689        if (icon == null) {
690            LogUtils.e(LOG_TAG, "Couldn't decode notif icon res id %d", resId);
691        }
692
693        return icon;
694    }
695
696    private static Bitmap getIcon(final Context context, final int resId) {
697        final Bitmap cachedIcon = sNotificationIcons.get(resId);
698        if (cachedIcon != null) {
699            return cachedIcon;
700        }
701
702        final Bitmap icon = BitmapFactory.decodeResource(context.getResources(), resId);
703        sNotificationIcons.put(resId, icon);
704
705        return icon;
706    }
707
708    private static void configureLatestEventInfoFromConversation(final Context context,
709            final Account account, final FolderPreferences folderPreferences,
710            final NotificationCompat.Builder notification, final Cursor conversationCursor,
711            final PendingIntent clickIntent, final Intent notificationIntent,
712            final String notificationAccount, final int unreadCount, final int unseenCount,
713            final Folder folder, final long when) {
714        final Resources res = context.getResources();
715
716        LogUtils.w(LOG_TAG, "NotificationUtils: Showing notification with unreadCount of %d and "
717                + "unseenCount of %d", unreadCount, unseenCount);
718
719        String notificationTicker = null;
720
721        // Boolean indicating that this notification is for a non-inbox label.
722        final boolean isInbox = account.settings.defaultInbox.equals(folder.uri);
723
724        // Notification label name for user label notifications.
725        final String notificationLabelName = isInbox ? null : folder.name;
726
727        if (unseenCount > 1) {
728            // Build the string that describes the number of new messages
729            final String newMessagesString = res.getString(R.string.new_messages, unseenCount);
730
731            // Use the default notification icon
732            notification.setLargeIcon(
733                    getDefaultNotificationIcon(context, folder, true /* multiple new messages */));
734
735            // The ticker initially start as the new messages string.
736            notificationTicker = newMessagesString;
737
738            // The title of the notification is the new messages string
739            notification.setContentTitle(newMessagesString);
740
741            // TODO(skennedy) Can we remove this check?
742            if (com.android.mail.utils.Utils.isRunningJellybeanOrLater()) {
743                // For a new-style notification
744                final int maxNumDigestItems = context.getResources().getInteger(
745                        R.integer.max_num_notification_digest_items);
746
747                // The body of the notification is the account name, or the label name.
748                notification.setSubText(isInbox ? notificationAccount : notificationLabelName);
749
750                final NotificationCompat.InboxStyle digest =
751                        new NotificationCompat.InboxStyle(notification);
752
753                int numDigestItems = 0;
754                do {
755                    final Conversation conversation = new Conversation(conversationCursor);
756
757                    if (!conversation.read) {
758                        boolean multipleUnreadThread = false;
759                        // TODO(cwren) extract this pattern into a helper
760
761                        Cursor cursor = null;
762                        MessageCursor messageCursor = null;
763                        try {
764                            final Uri.Builder uriBuilder = conversation.messageListUri.buildUpon();
765                            uriBuilder.appendQueryParameter(
766                                    UIProvider.LABEL_QUERY_PARAMETER, notificationLabelName);
767                            cursor = context.getContentResolver().query(uriBuilder.build(),
768                                    UIProvider.MESSAGE_PROJECTION, null, null, null);
769                            messageCursor = new MessageCursor(cursor);
770
771                            String from = "";
772                            String fromAddress = "";
773                            if (messageCursor.moveToPosition(messageCursor.getCount() - 1)) {
774                                final Message message = messageCursor.getMessage();
775                                fromAddress = message.getFrom();
776                                if (fromAddress == null) {
777                                    fromAddress = "";
778                                }
779                                from = getDisplayableSender(fromAddress);
780                            }
781                            while (messageCursor.moveToPosition(messageCursor.getPosition() - 1)) {
782                                final Message message = messageCursor.getMessage();
783                                if (!message.read &&
784                                        !fromAddress.contentEquals(message.getFrom())) {
785                                    multipleUnreadThread = true;
786                                    break;
787                                }
788                            }
789                            final SpannableStringBuilder sendersBuilder;
790                            if (multipleUnreadThread) {
791                                final int sendersLength =
792                                        res.getInteger(R.integer.swipe_senders_length);
793
794                                sendersBuilder = getStyledSenders(context, conversationCursor,
795                                        sendersLength, notificationAccount);
796                            } else {
797                                if (from == null) {
798                                    LogUtils.e(LOG_TAG, "NotificationUtils: null from string in " +
799                                            "configureLatestEventInfoFromConversation");
800                                    from = "";
801                                }
802                                sendersBuilder = new SpannableStringBuilder(from);
803                            }
804                            final CharSequence digestLine = getSingleMessageInboxLine(context,
805                                    sendersBuilder.toString(),
806                                    conversation.subject,
807                                    conversation.snippet);
808                            digest.addLine(digestLine);
809                            numDigestItems++;
810                        } finally {
811                            if (messageCursor != null) {
812                                messageCursor.close();
813                            }
814                            if (cursor != null) {
815                                cursor.close();
816                            }
817                        }
818                    }
819                } while (numDigestItems <= maxNumDigestItems && conversationCursor.moveToNext());
820            } else {
821                // The body of the notification is the account name, or the label name.
822                notification.setContentText(
823                        isInbox ? notificationAccount : notificationLabelName);
824            }
825        } else {
826            // For notifications for a single new conversation, we want to get the information from
827            // the conversation
828
829            // Move the cursor to the most recent unread conversation
830            seekToLatestUnreadConversation(conversationCursor);
831
832            final Conversation conversation = new Conversation(conversationCursor);
833
834            Cursor cursor = null;
835            MessageCursor messageCursor = null;
836            boolean multipleUnseenThread = false;
837            String from = null;
838            try {
839                final Uri uri = conversation.messageListUri.buildUpon().appendQueryParameter(
840                        UIProvider.LABEL_QUERY_PARAMETER, folder.persistentId).build();
841                cursor = context.getContentResolver().query(uri, UIProvider.MESSAGE_PROJECTION,
842                        null, null, null);
843                messageCursor = new MessageCursor(cursor);
844                // Use the information from the last sender in the conversation that triggered
845                // this notification.
846
847                String fromAddress = "";
848                if (messageCursor.moveToPosition(messageCursor.getCount() - 1)) {
849                    final Message message = messageCursor.getMessage();
850                    fromAddress = message.getFrom();
851                    from = getDisplayableSender(fromAddress);
852                    notification.setLargeIcon(
853                            getContactIcon(context, from, getSenderAddress(fromAddress), folder));
854                }
855
856                // Assume that the last message in this conversation is unread
857                int firstUnseenMessagePos = messageCursor.getPosition();
858                while (messageCursor.moveToPosition(messageCursor.getPosition() - 1)) {
859                    final Message message = messageCursor.getMessage();
860                    final boolean unseen = !message.seen;
861                    if (unseen) {
862                        firstUnseenMessagePos = messageCursor.getPosition();
863                        if (!multipleUnseenThread
864                                && !fromAddress.contentEquals(message.getFrom())) {
865                            multipleUnseenThread = true;
866                        }
867                    }
868                }
869
870                // TODO(skennedy) Can we remove this check?
871                if (Utils.isRunningJellybeanOrLater()) {
872                    // For a new-style notification
873
874                    if (multipleUnseenThread) {
875                        // The title of a single conversation is the list of senders.
876                        int sendersLength = res.getInteger(R.integer.swipe_senders_length);
877
878                        final SpannableStringBuilder sendersBuilder = getStyledSenders(
879                                context, conversationCursor, sendersLength, notificationAccount);
880
881                        notification.setContentTitle(sendersBuilder);
882                        // For a single new conversation, the ticker is based on the sender's name.
883                        notificationTicker = sendersBuilder.toString();
884                    } else {
885                        // The title of a single message the sender.
886                        notification.setContentTitle(from);
887                        // For a single new conversation, the ticker is based on the sender's name.
888                        notificationTicker = from;
889                    }
890
891                    // The notification content will be the subject of the conversation.
892                    notification.setContentText(
893                            getSingleMessageLittleText(context, conversation.subject));
894
895                    // The notification subtext will be the subject of the conversation for inbox
896                    // notifications, or will based on the the label name for user label
897                    // notifications.
898                    notification.setSubText(isInbox ? notificationAccount : notificationLabelName);
899
900                    if (multipleUnseenThread) {
901                        notification.setLargeIcon(
902                                getDefaultNotificationIcon(context, folder, true));
903                    }
904                    final NotificationCompat.BigTextStyle bigText =
905                            new NotificationCompat.BigTextStyle(notification);
906
907                    // Seek the message cursor to the first unread message
908                    final Message message;
909                    if (messageCursor.moveToPosition(firstUnseenMessagePos)) {
910                        message = messageCursor.getMessage();
911                        bigText.bigText(getSingleMessageBigText(context,
912                                conversation.subject, message));
913                    } else {
914                        LogUtils.e(LOG_TAG, "NotificationUtils: Failed to load message");
915                        message = null;
916                    }
917
918                    if (message != null) {
919                        final Set<String> notificationActions =
920                                folderPreferences.getNotificationActions(account);
921
922                        final int notificationId = getNotificationId(notificationAccount, folder);
923
924                        NotificationActionUtils.addNotificationActions(context, notificationIntent,
925                                notification, account, conversation, message, folder,
926                                notificationId, when, notificationActions);
927                    }
928                } else {
929                    // For an old-style notification
930
931                    // The title of a single conversation notification is built from both the sender
932                    // and subject of the new message.
933                    notification.setContentTitle(getSingleMessageNotificationTitle(context,
934                            from, conversation.subject));
935
936                    // The notification content will be the subject of the conversation for inbox
937                    // notifications, or will based on the the label name for user label
938                    // notifications.
939                    notification.setContentText(
940                            isInbox ? notificationAccount : notificationLabelName);
941
942                    // For a single new conversation, the ticker is based on the sender's name.
943                    notificationTicker = from;
944                }
945            } finally {
946                if (messageCursor != null) {
947                    messageCursor.close();
948                }
949                if (cursor != null) {
950                    cursor.close();
951                }
952            }
953        }
954
955        // Build the notification ticker
956        if (notificationLabelName != null && notificationTicker != null) {
957            // This is a per label notification, format the ticker with that information
958            notificationTicker = res.getString(R.string.label_notification_ticker,
959                    notificationLabelName, notificationTicker);
960        }
961
962        if (notificationTicker != null) {
963            // If we didn't generate a notification ticker, it will default to account name
964            notification.setTicker(notificationTicker);
965        }
966
967        // Set the number in the notification
968        if (unreadCount > 1) {
969            notification.setNumber(unreadCount);
970        }
971
972        notification.setContentIntent(clickIntent);
973    }
974
975    private static SpannableStringBuilder getStyledSenders(final Context context,
976            final Cursor conversationCursor, final int maxLength, final String account) {
977        final Conversation conversation = new Conversation(conversationCursor);
978        final com.android.mail.providers.ConversationInfo conversationInfo =
979                conversation.conversationInfo;
980        final ArrayList<SpannableString> senders = new ArrayList<SpannableString>();
981        if (sNotificationUnreadStyleSpan == null) {
982            sNotificationUnreadStyleSpan = new TextAppearanceSpan(
983                    context, R.style.NotificationSendersUnreadTextAppearance);
984            sNotificationReadStyleSpan =
985                    new TextAppearanceSpan(context, R.style.NotificationSendersReadTextAppearance);
986        }
987        SendersView.format(context, conversationInfo, "", maxLength, senders, null, null, account,
988                sNotificationUnreadStyleSpan, sNotificationReadStyleSpan, false);
989
990        return ellipsizeStyledSenders(context, senders);
991    }
992
993    private static String sSendersSplitToken = null;
994    private static String sElidedPaddingToken = null;
995
996    private static SpannableStringBuilder ellipsizeStyledSenders(final Context context,
997            ArrayList<SpannableString> styledSenders) {
998        if (sSendersSplitToken == null) {
999            sSendersSplitToken = context.getString(R.string.senders_split_token);
1000            sElidedPaddingToken = context.getString(R.string.elided_padding_token);
1001        }
1002
1003        SpannableStringBuilder builder = new SpannableStringBuilder();
1004        SpannableString prevSender = null;
1005        for (SpannableString sender : styledSenders) {
1006            if (sender == null) {
1007                LogUtils.e(LOG_TAG, "NotificationUtils: null sender iterating over styledSenders");
1008                continue;
1009            }
1010            CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class);
1011            if (SendersView.sElidedString.equals(sender.toString())) {
1012                prevSender = sender;
1013                sender = copyStyles(spans, sElidedPaddingToken + sender + sElidedPaddingToken);
1014            } else if (builder.length() > 0
1015                    && (prevSender == null || !SendersView.sElidedString.equals(prevSender
1016                            .toString()))) {
1017                prevSender = sender;
1018                sender = copyStyles(spans, sSendersSplitToken + sender);
1019            } else {
1020                prevSender = sender;
1021            }
1022            builder.append(sender);
1023        }
1024        return builder;
1025    }
1026
1027    private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) {
1028        SpannableString s = new SpannableString(newText);
1029        if (spans != null && spans.length > 0) {
1030            s.setSpan(spans[0], 0, s.length(), 0);
1031        }
1032        return s;
1033    }
1034
1035    /**
1036     * Seeks the cursor to the position of the most recent unread conversation. If no unread
1037     * conversation is found, the position of the cursor will be restored, and false will be
1038     * returned.
1039     */
1040    private static boolean seekToLatestUnreadConversation(final Cursor cursor) {
1041        final int initialPosition = cursor.getPosition();
1042        do {
1043            final Conversation conversation = new Conversation(cursor);
1044            if (!conversation.read) {
1045                return true;
1046            }
1047        } while (cursor.moveToNext());
1048
1049        // Didn't find an unread conversation, reset the position.
1050        cursor.moveToPosition(initialPosition);
1051        return false;
1052    }
1053
1054    /**
1055     * Sets the bigtext for a notification for a single new conversation
1056     *
1057     * @param context
1058     * @param senders Sender of the new message that triggered the notification.
1059     * @param subject Subject of the new message that triggered the notification
1060     * @param snippet Snippet of the new message that triggered the notification
1061     * @return a {@link CharSequence} suitable for use in
1062     *         {@link android.support.v4.app.NotificationCompat.BigTextStyle}
1063     */
1064    private static CharSequence getSingleMessageInboxLine(Context context,
1065            String senders, String subject, String snippet) {
1066        // TODO(cwren) finish this step toward commmon code with getSingleMessageBigText
1067
1068        final String subjectSnippet = !TextUtils.isEmpty(subject) ? subject : snippet;
1069
1070        final TextAppearanceSpan notificationPrimarySpan =
1071                new TextAppearanceSpan(context, R.style.NotificationPrimaryText);
1072
1073        if (TextUtils.isEmpty(senders)) {
1074            // If the senders are empty, just use the subject/snippet.
1075            return subjectSnippet;
1076        } else if (TextUtils.isEmpty(subjectSnippet)) {
1077            // If the subject/snippet is empty, just use the senders.
1078            final SpannableString spannableString = new SpannableString(senders);
1079            spannableString.setSpan(notificationPrimarySpan, 0, senders.length(), 0);
1080
1081            return spannableString;
1082        } else {
1083            final String formatString = context.getResources().getString(
1084                    R.string.multiple_new_message_notification_item);
1085            final TextAppearanceSpan notificationSecondarySpan =
1086                    new TextAppearanceSpan(context, R.style.NotificationSecondaryText);
1087
1088            final String instantiatedString = String.format(formatString,
1089                    sBidiFormatter.unicodeWrap(senders),
1090                    sBidiFormatter.unicodeWrap(subjectSnippet));
1091
1092            final SpannableString spannableString = new SpannableString(instantiatedString);
1093
1094            final boolean isOrderReversed = formatString.indexOf("%2$s") <
1095                    formatString.indexOf("%1$s");
1096            final int primaryOffset =
1097                    (isOrderReversed ? instantiatedString.lastIndexOf(senders) :
1098                     instantiatedString.indexOf(senders));
1099            final int secondaryOffset =
1100                    (isOrderReversed ? instantiatedString.lastIndexOf(subjectSnippet) :
1101                     instantiatedString.indexOf(subjectSnippet));
1102            spannableString.setSpan(notificationPrimarySpan,
1103                    primaryOffset, primaryOffset + senders.length(), 0);
1104            spannableString.setSpan(notificationSecondarySpan,
1105                    secondaryOffset, secondaryOffset + subjectSnippet.length(), 0);
1106            return spannableString;
1107        }
1108    }
1109
1110    /**
1111     * Sets the bigtext for a notification for a single new conversation
1112     * @param context
1113     * @param subject Subject of the new message that triggered the notification
1114     * @return a {@link CharSequence} suitable for use in
1115     * {@link NotificationCompat.Builder#setContentText}
1116     */
1117    private static CharSequence getSingleMessageLittleText(Context context, String subject) {
1118        final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
1119                context, R.style.NotificationPrimaryText);
1120
1121        final SpannableString spannableString = new SpannableString(subject);
1122        spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0);
1123
1124        return spannableString;
1125    }
1126
1127    /**
1128     * Sets the bigtext for a notification for a single new conversation
1129     *
1130     * @param context
1131     * @param subject Subject of the new message that triggered the notification
1132     * @param message the {@link Message} to be displayed.
1133     * @return a {@link CharSequence} suitable for use in
1134     *         {@link android.support.v4.app.NotificationCompat.BigTextStyle}
1135     */
1136    private static CharSequence getSingleMessageBigText(Context context, String subject,
1137            final Message message) {
1138
1139        final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
1140                context, R.style.NotificationPrimaryText);
1141
1142        final String snippet = getMessageBodyWithoutElidedText(message);
1143
1144        // Change multiple newlines (with potential white space between), into a single new line
1145        final String collapsedSnippet =
1146                !TextUtils.isEmpty(snippet) ? snippet.replaceAll("\\n\\s+", "\n") : "";
1147
1148        if (TextUtils.isEmpty(subject)) {
1149            // If the subject is empty, just use the snippet.
1150            return snippet;
1151        } else if (TextUtils.isEmpty(collapsedSnippet)) {
1152            // If the snippet is empty, just use the subject.
1153            final SpannableString spannableString = new SpannableString(subject);
1154            spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0);
1155
1156            return spannableString;
1157        } else {
1158            final String notificationBigTextFormat = context.getResources().getString(
1159                    R.string.single_new_message_notification_big_text);
1160
1161            // Localizers may change the order of the parameters, look at how the format
1162            // string is structured.
1163            final boolean isSubjectFirst = notificationBigTextFormat.indexOf("%2$s") >
1164                    notificationBigTextFormat.indexOf("%1$s");
1165            final String bigText =
1166                    String.format(notificationBigTextFormat, subject, collapsedSnippet);
1167            final SpannableString spannableString = new SpannableString(bigText);
1168
1169            final int subjectOffset =
1170                    (isSubjectFirst ? bigText.indexOf(subject) : bigText.lastIndexOf(subject));
1171            spannableString.setSpan(notificationSubjectSpan,
1172                    subjectOffset, subjectOffset + subject.length(), 0);
1173
1174            return spannableString;
1175        }
1176    }
1177
1178    /**
1179     * Gets the title for a notification for a single new conversation
1180     * @param context
1181     * @param sender Sender of the new message that triggered the notification.
1182     * @param subject Subject of the new message that triggered the notification
1183     * @return a {@link CharSequence} suitable for use as a {@link Notification} title.
1184     */
1185    private static CharSequence getSingleMessageNotificationTitle(Context context,
1186            String sender, String subject) {
1187
1188        if (TextUtils.isEmpty(subject)) {
1189            // If the subject is empty, just set the title to the sender's information.
1190            return sender;
1191        } else {
1192            final String notificationTitleFormat = context.getResources().getString(
1193                    R.string.single_new_message_notification_title);
1194
1195            // Localizers may change the order of the parameters, look at how the format
1196            // string is structured.
1197            final boolean isSubjectLast = notificationTitleFormat.indexOf("%2$s") >
1198                    notificationTitleFormat.indexOf("%1$s");
1199            final String titleString = String.format(notificationTitleFormat, sender, subject);
1200
1201            // Format the string so the subject is using the secondaryText style
1202            final SpannableString titleSpannable = new SpannableString(titleString);
1203
1204            // Find the offset of the subject.
1205            final int subjectOffset =
1206                    isSubjectLast ? titleString.lastIndexOf(subject) : titleString.indexOf(subject);
1207            final TextAppearanceSpan notificationSubjectSpan =
1208                    new TextAppearanceSpan(context, R.style.NotificationSecondaryText);
1209            titleSpannable.setSpan(notificationSubjectSpan,
1210                    subjectOffset, subjectOffset + subject.length(), 0);
1211            return titleSpannable;
1212        }
1213    }
1214
1215    /**
1216     * Clears the notifications for the specified account/folder.
1217     */
1218    public static void clearFolderNotification(Context context, Account account, Folder folder,
1219            final boolean markSeen) {
1220        LogUtils.v(LOG_TAG, "NotificationUtils: Clearing all notifications for %s/%s", account.name,
1221                folder.name);
1222        final NotificationMap notificationMap = getNotificationMap(context);
1223        final NotificationKey key = new NotificationKey(account, folder);
1224        notificationMap.remove(key);
1225        notificationMap.saveNotificationMap(context);
1226
1227        final NotificationManager notificationManager =
1228                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
1229        notificationManager.cancel(getNotificationId(account.name, folder));
1230
1231        if (markSeen) {
1232            markSeen(context, folder);
1233        }
1234    }
1235
1236    /**
1237     * Clears all notifications for the specified account.
1238     */
1239    public static void clearAccountNotifications(final Context context, final String account) {
1240        LogUtils.v(LOG_TAG, "NotificationUtils: Clearing all notifications for %s", account);
1241        final NotificationMap notificationMap = getNotificationMap(context);
1242
1243        // Find all NotificationKeys for this account
1244        final ImmutableList.Builder<NotificationKey> keyBuilder = ImmutableList.builder();
1245
1246        for (final NotificationKey key : notificationMap.keySet()) {
1247            if (account.equals(key.account.name)) {
1248                keyBuilder.add(key);
1249            }
1250        }
1251
1252        final List<NotificationKey> notificationKeys = keyBuilder.build();
1253
1254        final NotificationManager notificationManager =
1255                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
1256
1257        for (final NotificationKey notificationKey : notificationKeys) {
1258            final Folder folder = notificationKey.folder;
1259            notificationManager.cancel(getNotificationId(account, folder));
1260            notificationMap.remove(notificationKey);
1261        }
1262
1263        notificationMap.saveNotificationMap(context);
1264    }
1265
1266    private static ArrayList<Long> findContacts(Context context, Collection<String> addresses) {
1267        ArrayList<String> whereArgs = new ArrayList<String>();
1268        StringBuilder whereBuilder = new StringBuilder();
1269        String[] questionMarks = new String[addresses.size()];
1270
1271        whereArgs.addAll(addresses);
1272        Arrays.fill(questionMarks, "?");
1273        whereBuilder.append(Email.DATA1 + " IN (").
1274                append(TextUtils.join(",", questionMarks)).
1275                append(")");
1276
1277        ContentResolver resolver = context.getContentResolver();
1278        Cursor c = resolver.query(Email.CONTENT_URI,
1279                new String[]{Email.CONTACT_ID}, whereBuilder.toString(),
1280                whereArgs.toArray(new String[0]), null);
1281
1282        ArrayList<Long> contactIds = new ArrayList<Long>();
1283        if (c == null) {
1284            return contactIds;
1285        }
1286        try {
1287            while (c.moveToNext()) {
1288                contactIds.add(c.getLong(0));
1289            }
1290        } finally {
1291            c.close();
1292        }
1293        return contactIds;
1294    }
1295
1296    private static Bitmap getContactIcon(final Context context, final String displayName,
1297            final String senderAddress, final Folder folder) {
1298        if (senderAddress == null) {
1299            return null;
1300        }
1301
1302        Bitmap icon = null;
1303
1304        final List<Long> contactIds = findContacts( context, Arrays.asList(
1305                new String[] { senderAddress }));
1306
1307        // Get the ideal size for this icon.
1308        final Resources res = context.getResources();
1309        final int idealIconHeight =
1310                res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
1311        final int idealIconWidth =
1312                res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
1313
1314        if (contactIds != null) {
1315            for (final long id : contactIds) {
1316                final Uri contactUri =
1317                        ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id);
1318                final Uri photoUri = Uri.withAppendedPath(contactUri, Photo.CONTENT_DIRECTORY);
1319                final Cursor cursor = context.getContentResolver().query(
1320                        photoUri, new String[] { Photo.PHOTO }, null, null, null);
1321
1322                if (cursor != null) {
1323                    try {
1324                        if (cursor.moveToFirst()) {
1325                            final byte[] data = cursor.getBlob(0);
1326                            if (data != null) {
1327                                icon = BitmapFactory.decodeStream(new ByteArrayInputStream(data));
1328                                if (icon != null && icon.getHeight() < idealIconHeight) {
1329                                    // We should scale this image to fit the intended size
1330                                    icon = Bitmap.createScaledBitmap(
1331                                            icon, idealIconWidth, idealIconHeight, true);
1332                                }
1333                                if (icon != null) {
1334                                    break;
1335                                }
1336                            }
1337                        }
1338                    } finally {
1339                        cursor.close();
1340                    }
1341                }
1342            }
1343        }
1344
1345        if (icon == null) {
1346            // Make a colorful tile!
1347            final ContactPhotoManager contactPhotoManager =
1348                    ContactPhotoManager.getInstance(context);
1349
1350            final Dimensions dimensions = new Dimensions(idealIconWidth, idealIconHeight,
1351                    Dimensions.SCALE_ONE);
1352
1353            icon = contactPhotoManager.getDefaultImageProvider().getLetterTile(dimensions,
1354                    displayName, senderAddress);
1355        }
1356
1357        if (icon == null) {
1358            // Icon should be the default mail icon.
1359            icon = getDefaultNotificationIcon(context, folder, false /* single new message */);
1360        }
1361        return icon;
1362    }
1363
1364    private static String getMessageBodyWithoutElidedText(final Message message) {
1365        return getMessageBodyWithoutElidedText(message.getBodyAsHtml());
1366    }
1367
1368    public static String getMessageBodyWithoutElidedText(String html) {
1369        if (TextUtils.isEmpty(html)) {
1370            return "";
1371        }
1372        // Get the html "tree" for this message body
1373        final HtmlTree htmlTree = com.android.mail.utils.Utils.getHtmlTree(html);
1374        htmlTree.setPlainTextConverterFactory(MESSAGE_CONVERTER_FACTORY);
1375
1376        return htmlTree.getPlainText();
1377    }
1378
1379    public static void markSeen(final Context context, final Folder folder) {
1380        final Uri uri = folder.uri;
1381
1382        final ContentValues values = new ContentValues(1);
1383        values.put(UIProvider.ConversationColumns.SEEN, 1);
1384
1385        context.getContentResolver().update(uri, values, null, null);
1386    }
1387
1388    /**
1389     * Returns a displayable string representing
1390     * the message sender. It has a preference toward showing the name,
1391     * but will fall back to the address if that is all that is available.
1392     */
1393    private static String getDisplayableSender(String sender) {
1394        final EmailAddress address = EmailAddress.getEmailAddress(sender);
1395
1396        String displayableSender = address.getName();
1397
1398        if (!TextUtils.isEmpty(displayableSender)) {
1399            return Address.decodeAddressName(displayableSender);
1400        }
1401
1402        // If that fails, default to the sender address.
1403        displayableSender = address.getAddress();
1404
1405        // If we were unable to tokenize a name or address,
1406        // just use whatever was in the sender.
1407        if (TextUtils.isEmpty(displayableSender)) {
1408            displayableSender = sender;
1409        }
1410        return displayableSender;
1411    }
1412
1413    /**
1414     * Returns only the address portion of a message sender.
1415     */
1416    private static String getSenderAddress(String sender) {
1417        final EmailAddress address = EmailAddress.getEmailAddress(sender);
1418
1419        String tokenizedAddress = address.getAddress();
1420
1421        // If we were unable to tokenize a name or address,
1422        // just use whatever was in the sender.
1423        if (TextUtils.isEmpty(tokenizedAddress)) {
1424            tokenizedAddress = sender;
1425        }
1426        return tokenizedAddress;
1427    }
1428
1429    public static int getNotificationId(final String account, final Folder folder) {
1430        return 1 ^ account.hashCode() ^ folder.hashCode();
1431    }
1432
1433    private static class NotificationKey {
1434        public final Account account;
1435        public final Folder folder;
1436
1437        public NotificationKey(Account account, Folder folder) {
1438            this.account = account;
1439            this.folder = folder;
1440        }
1441
1442        @Override
1443        public boolean equals(Object other) {
1444            if (!(other instanceof NotificationKey)) {
1445                return false;
1446            }
1447            NotificationKey key = (NotificationKey) other;
1448            return account.equals(key.account) && folder.equals(key.folder);
1449        }
1450
1451        @Override
1452        public String toString() {
1453            return account.name + " " + folder.name;
1454        }
1455
1456        @Override
1457        public int hashCode() {
1458            final int accountHashCode = account.hashCode();
1459            final int folderHashCode = folder.hashCode();
1460            return accountHashCode ^ folderHashCode;
1461        }
1462    }
1463
1464    /**
1465     * Contains the logic for converting the contents of one HtmlTree into
1466     * plaintext.
1467     */
1468    public static class MailMessagePlainTextConverter extends HtmlTree.DefaultPlainTextConverter {
1469        // Strings for parsing html message bodies
1470        private static final String ELIDED_TEXT_ELEMENT_NAME = "div";
1471        private static final String ELIDED_TEXT_ELEMENT_ATTRIBUTE_NAME = "class";
1472        private static final String ELIDED_TEXT_ELEMENT_ATTRIBUTE_CLASS_VALUE = "elided-text";
1473
1474        private static final HTML.Attribute ELIDED_TEXT_ATTRIBUTE =
1475                new HTML.Attribute(ELIDED_TEXT_ELEMENT_ATTRIBUTE_NAME, HTML.Attribute.NO_TYPE);
1476
1477        private static final HtmlDocument.Node ELIDED_TEXT_REPLACEMENT_NODE =
1478                HtmlDocument.createSelfTerminatingTag(HTML4.BR_ELEMENT, null, null, null);
1479
1480        private int mEndNodeElidedTextBlock = -1;
1481
1482        @Override
1483        public void addNode(HtmlDocument.Node n, int nodeNum, int endNum) {
1484            // If we are in the middle of an elided text block, don't add this node
1485            if (nodeNum < mEndNodeElidedTextBlock) {
1486                return;
1487            } else if (nodeNum == mEndNodeElidedTextBlock) {
1488                super.addNode(ELIDED_TEXT_REPLACEMENT_NODE, nodeNum, endNum);
1489                return;
1490            }
1491
1492            // If this tag starts another elided text block, we want to remember the end
1493            if (n instanceof HtmlDocument.Tag) {
1494                boolean foundElidedTextTag = false;
1495                final HtmlDocument.Tag htmlTag = (HtmlDocument.Tag)n;
1496                final HTML.Element htmlElement = htmlTag.getElement();
1497                if (ELIDED_TEXT_ELEMENT_NAME.equals(htmlElement.getName())) {
1498                    // Make sure that the class is what is expected
1499                    final List<HtmlDocument.TagAttribute> attributes =
1500                            htmlTag.getAttributes(ELIDED_TEXT_ATTRIBUTE);
1501                    for (HtmlDocument.TagAttribute attribute : attributes) {
1502                        if (ELIDED_TEXT_ELEMENT_ATTRIBUTE_CLASS_VALUE.equals(
1503                                attribute.getValue())) {
1504                            // Found an "elided-text" div.  Remember information about this tag
1505                            mEndNodeElidedTextBlock = endNum;
1506                            foundElidedTextTag = true;
1507                            break;
1508                        }
1509                    }
1510                }
1511
1512                if (foundElidedTextTag) {
1513                    return;
1514                }
1515            }
1516
1517            super.addNode(n, nodeNum, endNum);
1518        }
1519    }
1520
1521    /**
1522     * During account setup in Email, we may not have an inbox yet, so the notification setting had
1523     * to be stored in {@link AccountPreferences}. If it is still there, we need to move it to the
1524     * {@link FolderPreferences} now.
1525     */
1526    public static void moveNotificationSetting(final AccountPreferences accountPreferences,
1527            final FolderPreferences folderPreferences) {
1528        if (accountPreferences.isDefaultInboxNotificationsEnabledSet()) {
1529            // If this setting has been changed some other way, don't overwrite it
1530            if (!folderPreferences.isNotificationsEnabledSet()) {
1531                final boolean notificationsEnabled =
1532                        accountPreferences.getDefaultInboxNotificationsEnabled();
1533
1534                folderPreferences.setNotificationsEnabled(notificationsEnabled);
1535            }
1536
1537            accountPreferences.clearDefaultInboxNotificationsEnabled();
1538        }
1539    }
1540}
1541