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.PendingIntent;
20import android.content.ContentResolver;
21import android.content.ContentUris;
22import android.content.ContentValues;
23import android.content.Context;
24import android.content.Intent;
25import android.content.res.Resources;
26import android.database.Cursor;
27import android.graphics.Bitmap;
28import android.graphics.BitmapFactory;
29import android.net.MailTo;
30import android.net.Uri;
31import android.os.Looper;
32import android.provider.ContactsContract;
33import android.provider.ContactsContract.CommonDataKinds.Email;
34import android.support.v4.app.NotificationCompat;
35import android.support.v4.app.NotificationManagerCompat;
36import android.support.v4.text.BidiFormatter;
37import android.support.v4.util.ArrayMap;
38import android.text.SpannableString;
39import android.text.SpannableStringBuilder;
40import android.text.TextUtils;
41import android.text.style.CharacterStyle;
42import android.text.style.TextAppearanceSpan;
43import android.util.Pair;
44import android.util.SparseArray;
45
46import com.android.emailcommon.mail.Address;
47import com.android.mail.EmailAddress;
48import com.android.mail.MailIntentService;
49import com.android.mail.R;
50import com.android.mail.analytics.Analytics;
51import com.android.mail.browse.ConversationItemView;
52import com.android.mail.browse.MessageCursor;
53import com.android.mail.browse.SendersView;
54import com.android.mail.photo.ContactFetcher;
55import com.android.mail.photomanager.LetterTileProvider;
56import com.android.mail.preferences.AccountPreferences;
57import com.android.mail.preferences.FolderPreferences;
58import com.android.mail.preferences.MailPrefs;
59import com.android.mail.providers.Account;
60import com.android.mail.providers.Conversation;
61import com.android.mail.providers.Folder;
62import com.android.mail.providers.Message;
63import com.android.mail.providers.UIProvider;
64import com.android.mail.ui.ImageCanvas.Dimensions;
65import com.android.mail.utils.NotificationActionUtils.NotificationAction;
66import com.google.android.mail.common.html.parser.HTML;
67import com.google.android.mail.common.html.parser.HTML4;
68import com.google.android.mail.common.html.parser.HtmlDocument;
69import com.google.android.mail.common.html.parser.HtmlTree;
70import com.google.common.base.Objects;
71import com.google.common.collect.ImmutableList;
72import com.google.common.collect.Lists;
73import com.google.common.collect.Sets;
74import com.google.common.io.Closeables;
75
76import java.io.InputStream;
77import java.lang.ref.WeakReference;
78import java.util.ArrayList;
79import java.util.Arrays;
80import java.util.Collection;
81import java.util.HashMap;
82import java.util.HashSet;
83import java.util.List;
84import java.util.Map;
85import java.util.Set;
86import java.util.concurrent.ConcurrentHashMap;
87
88public class NotificationUtils {
89    public static final String LOG_TAG = "NotifUtils";
90
91    public static final String EXTRA_UNREAD_COUNT = "unread-count";
92    public static final String EXTRA_UNSEEN_COUNT = "unseen-count";
93    public static final String EXTRA_GET_ATTENTION = "get-attention";
94
95    /** Contains a list of <(account, label), unread conversations> */
96    private static NotificationMap sActiveNotificationMap = null;
97
98    private static final SparseArray<Bitmap> sNotificationIcons = new SparseArray<Bitmap>();
99    private static WeakReference<Bitmap> sDefaultWearableBg = new WeakReference<Bitmap>(null);
100
101    private static TextAppearanceSpan sNotificationUnreadStyleSpan;
102    private static CharacterStyle sNotificationReadStyleSpan;
103
104    /** A factory that produces a plain text converter that removes elided text. */
105    private static final HtmlTree.ConverterFactory MESSAGE_CONVERTER_FACTORY =
106            new HtmlTree.ConverterFactory() {
107                @Override
108                public HtmlTree.Converter<String> createInstance() {
109                    return new MailMessagePlainTextConverter();
110                }
111            };
112
113    private static BidiFormatter sBidiFormatter = BidiFormatter.getInstance();
114
115    // Maps summary notification to conversation notification ids.
116    private static Map<NotificationKey, Set<Integer>> sConversationNotificationMap =
117            new HashMap<NotificationKey, Set<Integer>>();
118
119    /**
120     * Clears all notifications in response to the user tapping "Clear" in the status bar.
121     */
122    public static void clearAllNotfications(Context context) {
123        LogUtils.v(LOG_TAG, "Clearing all notifications.");
124        final NotificationMap notificationMap = getNotificationMap(context);
125        notificationMap.clear();
126        notificationMap.saveNotificationMap(context);
127    }
128
129    /**
130     * Returns the notification map, creating it if necessary.
131     */
132    private static synchronized NotificationMap getNotificationMap(Context context) {
133        if (sActiveNotificationMap == null) {
134            sActiveNotificationMap = new NotificationMap();
135
136            // populate the map from the cached data
137            sActiveNotificationMap.loadNotificationMap(context);
138        }
139        return sActiveNotificationMap;
140    }
141
142    /**
143     * Class representing the existing notifications, and the number of unread and
144     * unseen conversations that triggered each.
145     */
146    private static final class NotificationMap {
147
148        private static final String NOTIFICATION_PART_SEPARATOR = " ";
149        private static final int NUM_NOTIFICATION_PARTS= 4;
150        private final ConcurrentHashMap<NotificationKey, Pair<Integer, Integer>> mMap =
151            new ConcurrentHashMap<NotificationKey, Pair<Integer, Integer>>();
152
153        /**
154         * Returns the number of key values pairs in the inner map.
155         */
156        public int size() {
157            return mMap.size();
158        }
159
160        /**
161         * Returns a set of key values.
162         */
163        public Set<NotificationKey> keySet() {
164            return mMap.keySet();
165        }
166
167        /**
168         * Remove the key from the inner map and return its value.
169         *
170         * @param key The key {@link NotificationKey} to be removed.
171         * @return The value associated with this key.
172         */
173        public Pair<Integer, Integer> remove(NotificationKey key) {
174            return mMap.remove(key);
175        }
176
177        /**
178         * Clear all key-value pairs in the map.
179         */
180        public void clear() {
181            mMap.clear();
182        }
183
184        /**
185         * Discover if a key-value pair with this key exists.
186         *
187         * @param key The key {@link NotificationKey} to be checked.
188         * @return If a key-value pair with this key exists in the map.
189         */
190        public boolean containsKey(NotificationKey key) {
191            return mMap.containsKey(key);
192        }
193
194        /**
195         * Returns the unread count for the given NotificationKey.
196         */
197        public Integer getUnread(NotificationKey key) {
198            final Pair<Integer, Integer> value = mMap.get(key);
199            return value != null ? value.first : null;
200        }
201
202        /**
203         * Returns the unread unseen count for the given NotificationKey.
204         */
205        public Integer getUnseen(NotificationKey key) {
206            final Pair<Integer, Integer> value = mMap.get(key);
207            return value != null ? value.second : null;
208        }
209
210        /**
211         * Store the unread and unseen value for the given NotificationKey
212         */
213        public void put(NotificationKey key, int unread, int unseen) {
214            final Pair<Integer, Integer> value =
215                    new Pair<Integer, Integer>(Integer.valueOf(unread), Integer.valueOf(unseen));
216            mMap.put(key, value);
217        }
218
219        /**
220         * Populates the notification map with previously cached data.
221         */
222        public synchronized void loadNotificationMap(final Context context) {
223            final MailPrefs mailPrefs = MailPrefs.get(context);
224            final Set<String> notificationSet = mailPrefs.getActiveNotificationSet();
225            if (notificationSet != null) {
226                for (String notificationEntry : notificationSet) {
227                    // Get the parts of the string that make the notification entry
228                    final String[] notificationParts =
229                            TextUtils.split(notificationEntry, NOTIFICATION_PART_SEPARATOR);
230                    if (notificationParts.length == NUM_NOTIFICATION_PARTS) {
231                        final Uri accountUri = Uri.parse(notificationParts[0]);
232                        final Cursor accountCursor = context.getContentResolver().query(
233                                accountUri, UIProvider.ACCOUNTS_PROJECTION, null, null, null);
234
235                        if (accountCursor == null) {
236                            throw new IllegalStateException("Unable to locate account for uri: " +
237                                    LogUtils.contentUriToString(accountUri));
238                        }
239
240                        final Account account;
241                        try {
242                            if (accountCursor.moveToFirst()) {
243                                account = Account.builder().buildFrom(accountCursor);
244                            } else {
245                                continue;
246                            }
247                        } finally {
248                            accountCursor.close();
249                        }
250
251                        final Uri folderUri = Uri.parse(notificationParts[1]);
252                        final Cursor folderCursor = context.getContentResolver().query(
253                                folderUri, UIProvider.FOLDERS_PROJECTION, null, null, null);
254
255                        if (folderCursor == null) {
256                            throw new IllegalStateException("Unable to locate folder for uri: " +
257                                    LogUtils.contentUriToString(folderUri));
258                        }
259
260                        final Folder folder;
261                        try {
262                            if (folderCursor.moveToFirst()) {
263                                folder = new Folder(folderCursor);
264                            } else {
265                                continue;
266                            }
267                        } finally {
268                            folderCursor.close();
269                        }
270
271                        final NotificationKey key = new NotificationKey(account, folder);
272                        final Integer unreadValue = Integer.valueOf(notificationParts[2]);
273                        final Integer unseenValue = Integer.valueOf(notificationParts[3]);
274                        put(key, unreadValue, unseenValue);
275                    }
276                }
277            }
278        }
279
280        /**
281         * Cache the notification map.
282         */
283        public synchronized void saveNotificationMap(Context context) {
284            final Set<String> notificationSet = Sets.newHashSet();
285            final Set<NotificationKey> keys = keySet();
286            for (NotificationKey key : keys) {
287                final Integer unreadCount = getUnread(key);
288                final Integer unseenCount = getUnseen(key);
289                if (unreadCount != null && unseenCount != null) {
290                    final String[] partValues = new String[] {
291                            key.account.uri.toString(), key.folder.folderUri.fullUri.toString(),
292                            unreadCount.toString(), unseenCount.toString()};
293                    notificationSet.add(TextUtils.join(NOTIFICATION_PART_SEPARATOR, partValues));
294                }
295            }
296            final MailPrefs mailPrefs = MailPrefs.get(context);
297            mailPrefs.cacheActiveNotificationSet(notificationSet);
298        }
299    }
300
301    /**
302     * @return the title of this notification with each account and the number of unread and unseen
303     * conversations for it. Also remove any account in the map that has 0 unread.
304     */
305    private static String createNotificationString(NotificationMap notifications) {
306        StringBuilder result = new StringBuilder();
307        int i = 0;
308        Set<NotificationKey> keysToRemove = Sets.newHashSet();
309        for (NotificationKey key : notifications.keySet()) {
310            Integer unread = notifications.getUnread(key);
311            Integer unseen = notifications.getUnseen(key);
312            if (unread == null || unread.intValue() == 0) {
313                keysToRemove.add(key);
314            } else {
315                if (i > 0) result.append(", ");
316                result.append(key.toString() + " (" + unread + ", " + unseen + ")");
317                i++;
318            }
319        }
320
321        for (NotificationKey key : keysToRemove) {
322            notifications.remove(key);
323        }
324
325        return result.toString();
326    }
327
328    /**
329     * Get all notifications for all accounts and cancel them.
330     **/
331    public static void cancelAllNotifications(Context context) {
332        LogUtils.d(LOG_TAG, "cancelAllNotifications - cancelling all");
333        NotificationManagerCompat nm = NotificationManagerCompat.from(context);
334        nm.cancelAll();
335        clearAllNotfications(context);
336    }
337
338    /**
339     * Get all notifications for all accounts, cancel them, and repost.
340     * This happens when locale changes.
341     **/
342    public static void cancelAndResendNotificationsOnLocaleChange(
343            Context context, final ContactFetcher contactFetcher) {
344        LogUtils.d(LOG_TAG, "cancelAndResendNotificationsOnLocaleChange");
345        sBidiFormatter = BidiFormatter.getInstance();
346        resendNotifications(context, true, null, null, contactFetcher);
347    }
348
349    /**
350     * Get all notifications for all accounts, optionally cancel them, and repost.
351     * This happens when locale changes. If you only want to resend messages from one
352     * account-folder pair, pass in the account and folder that should be resent.
353     * All other account-folder pairs will not have their notifications resent.
354     * All notifications will be resent if account or folder is null.
355     *
356     * @param context Current context.
357     * @param cancelExisting True, if all notifications should be canceled before resending.
358     *                       False, otherwise.
359     * @param accountUri The {@link Uri} of the {@link Account} of the notification
360     *                   upon which an action occurred, or {@code null}.
361     * @param folderUri The {@link Uri} of the {@link Folder} of the notification
362     *                  upon which an action occurred, or {@code null}.
363     */
364    public static void resendNotifications(Context context, final boolean cancelExisting,
365            final Uri accountUri, final FolderUri folderUri,
366            final ContactFetcher contactFetcher) {
367        LogUtils.i(LOG_TAG, "resendNotifications cancelExisting: %b, account: %s, folder: %s",
368                cancelExisting,
369                accountUri == null ? null : LogUtils.sanitizeName(LOG_TAG, accountUri.toString()),
370                folderUri == null ? null : LogUtils.sanitizeName(LOG_TAG, folderUri.toString()));
371
372        if (cancelExisting) {
373            LogUtils.d(LOG_TAG, "resendNotifications - cancelling all");
374            NotificationManagerCompat nm = NotificationManagerCompat.from(context);
375            nm.cancelAll();
376        }
377        // Re-validate the notifications.
378        final NotificationMap notificationMap = getNotificationMap(context);
379        final Set<NotificationKey> keys = notificationMap.keySet();
380        for (NotificationKey notification : keys) {
381            final Folder folder = notification.folder;
382            final int notificationId =
383                    getNotificationId(notification.account.getAccountManagerAccount(), folder);
384
385            // Only resend notifications if the notifications are from the same folder
386            // and same account as the undo notification that was previously displayed.
387            if (accountUri != null && !Objects.equal(accountUri, notification.account.uri) &&
388                    folderUri != null && !Objects.equal(folderUri, folder.folderUri)) {
389                LogUtils.d(LOG_TAG, "resendNotifications - not resending %s / %s"
390                        + " because it doesn't match %s / %s",
391                        notification.account.uri, folder.folderUri, accountUri, folderUri);
392                continue;
393            }
394
395            LogUtils.d(LOG_TAG, "resendNotifications - resending %s / %s",
396                    notification.account.uri, folder.folderUri);
397
398            final NotificationAction undoableAction =
399                    NotificationActionUtils.sUndoNotifications.get(notificationId);
400            if (undoableAction == null) {
401                validateNotifications(context, folder, notification.account, true,
402                        false, notification, contactFetcher);
403            } else {
404                // Create an undo notification
405                NotificationActionUtils.createUndoNotification(context, undoableAction);
406            }
407        }
408    }
409
410    /**
411     * Validate the notifications for the specified account.
412     */
413    public static void validateAccountNotifications(Context context, Account account) {
414        final String email = account.getEmailAddress();
415        LogUtils.d(LOG_TAG, "validateAccountNotifications - %s", email);
416
417        List<NotificationKey> notificationsToCancel = Lists.newArrayList();
418        // Iterate through the notification map to see if there are any entries that correspond to
419        // labels that are not in the sync set.
420        final NotificationMap notificationMap = getNotificationMap(context);
421        Set<NotificationKey> keys = notificationMap.keySet();
422        final AccountPreferences accountPreferences = new AccountPreferences(context,
423                account.getAccountId());
424        final boolean enabled = accountPreferences.areNotificationsEnabled();
425        if (!enabled) {
426            // Cancel all notifications for this account
427            for (NotificationKey notification : keys) {
428                if (notification.account.getAccountManagerAccount().name.equals(email)) {
429                    notificationsToCancel.add(notification);
430                }
431            }
432        } else {
433            // Iterate through the notification map to see if there are any entries that
434            // correspond to labels that are not in the notification set.
435            for (NotificationKey notification : keys) {
436                if (notification.account.getAccountManagerAccount().name.equals(email)) {
437                    // If notification is not enabled for this label, remember this NotificationKey
438                    // to later cancel the notification, and remove the entry from the map
439                    final Folder folder = notification.folder;
440                    final boolean isInbox = folder.folderUri.equals(
441                            notification.account.settings.defaultInbox);
442                    final FolderPreferences folderPreferences = new FolderPreferences(
443                            context, notification.account.getAccountId(), folder, isInbox);
444
445                    if (!folderPreferences.areNotificationsEnabled()) {
446                        notificationsToCancel.add(notification);
447                    }
448                }
449            }
450        }
451
452        // Cancel & remove the invalid notifications.
453        if (notificationsToCancel.size() > 0) {
454            NotificationManagerCompat nm = NotificationManagerCompat.from(context);
455            for (NotificationKey notification : notificationsToCancel) {
456                final Folder folder = notification.folder;
457                final int notificationId =
458                        getNotificationId(notification.account.getAccountManagerAccount(), folder);
459                LogUtils.d(LOG_TAG, "validateAccountNotifications - cancelling %s / %s",
460                        notification.account.getEmailAddress(), folder.persistentId);
461                nm.cancel(notificationId);
462                notificationMap.remove(notification);
463                NotificationActionUtils.sUndoNotifications.remove(notificationId);
464                NotificationActionUtils.sNotificationTimestamps.delete(notificationId);
465
466                cancelConversationNotifications(notification, nm);
467            }
468            notificationMap.saveNotificationMap(context);
469        }
470    }
471
472    public static void sendSetNewEmailIndicatorIntent(Context context, final int unreadCount,
473            final int unseenCount, final Account account, final Folder folder,
474            final boolean getAttention) {
475        LogUtils.i(LOG_TAG, "sendSetNewEmailIndicator account: %s, folder: %s",
476                LogUtils.sanitizeName(LOG_TAG, account.getEmailAddress()),
477                LogUtils.sanitizeName(LOG_TAG, folder.name));
478
479        final Intent intent = new Intent(MailIntentService.ACTION_SEND_SET_NEW_EMAIL_INDICATOR);
480        intent.setPackage(context.getPackageName()); // Make sure we only deliver this to ourselves
481        intent.putExtra(EXTRA_UNREAD_COUNT, unreadCount);
482        intent.putExtra(EXTRA_UNSEEN_COUNT, unseenCount);
483        intent.putExtra(Utils.EXTRA_ACCOUNT, account);
484        intent.putExtra(Utils.EXTRA_FOLDER, folder);
485        intent.putExtra(EXTRA_GET_ATTENTION, getAttention);
486        context.startService(intent);
487    }
488
489    /**
490     * Display only one notification. Should only be called from
491     * {@link com.android.mail.MailIntentService}. Use {@link #sendSetNewEmailIndicatorIntent}
492     * if you need to perform this action anywhere else.
493     */
494    public static void setNewEmailIndicator(Context context, final int unreadCount,
495            final int unseenCount, final Account account, final Folder folder,
496            final boolean getAttention, final ContactFetcher contactFetcher) {
497        LogUtils.d(LOG_TAG, "setNewEmailIndicator unreadCount = %d, unseenCount = %d, account = %s,"
498                + " folder = %s, getAttention = %b", unreadCount, unseenCount,
499                account.getEmailAddress(), folder.folderUri, getAttention);
500
501        boolean ignoreUnobtrusiveSetting = false;
502
503        final int notificationId = getNotificationId(account.getAccountManagerAccount(), folder);
504
505        // Update the notification map
506        final NotificationMap notificationMap = getNotificationMap(context);
507        final NotificationKey key = new NotificationKey(account, folder);
508        if (unreadCount == 0) {
509            LogUtils.d(LOG_TAG, "setNewEmailIndicator - cancelling %s / %s",
510                    account.getEmailAddress(), folder.persistentId);
511            notificationMap.remove(key);
512
513            NotificationManagerCompat nm = NotificationManagerCompat.from(context);
514            nm.cancel(notificationId);
515            cancelConversationNotifications(key, nm);
516        } else {
517            LogUtils.d(LOG_TAG, "setNewEmailIndicator - update count for: %s / %s " +
518                    "to: unread: %d unseen %d", account.getEmailAddress(), folder.persistentId,
519                    unreadCount, unseenCount);
520            if (!notificationMap.containsKey(key)) {
521                // This account previously didn't have any unread mail; ignore the "unobtrusive
522                // notifications" setting and play sound and/or vibrate the device even if a
523                // notification already exists (bug 2412348).
524                LogUtils.d(LOG_TAG, "setNewEmailIndicator - ignoringUnobtrusiveSetting");
525                ignoreUnobtrusiveSetting = true;
526            }
527            notificationMap.put(key, unreadCount, unseenCount);
528        }
529        notificationMap.saveNotificationMap(context);
530
531        if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
532            LogUtils.v(LOG_TAG, "New email: %s mapSize: %d getAttention: %b",
533                    createNotificationString(notificationMap), notificationMap.size(),
534                    getAttention);
535        }
536
537        if (NotificationActionUtils.sUndoNotifications.get(notificationId) == null) {
538            validateNotifications(context, folder, account, getAttention, ignoreUnobtrusiveSetting,
539                    key, contactFetcher);
540        }
541    }
542
543    /**
544     * Validate the notifications notification.
545     */
546    private static void validateNotifications(Context context, final Folder folder,
547            final Account account, boolean getAttention, boolean ignoreUnobtrusiveSetting,
548            NotificationKey key, final ContactFetcher contactFetcher) {
549
550        NotificationManagerCompat nm = NotificationManagerCompat.from(context);
551
552        final NotificationMap notificationMap = getNotificationMap(context);
553        if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
554            LogUtils.i(LOG_TAG, "Validating Notification: %s mapSize: %d "
555                    + "folder: %s getAttention: %b ignoreUnobtrusive: %b",
556                    createNotificationString(notificationMap),
557                    notificationMap.size(), folder.name, getAttention, ignoreUnobtrusiveSetting);
558        } else {
559            LogUtils.i(LOG_TAG, "Validating Notification, mapSize: %d "
560                    + "getAttention: %b ignoreUnobtrusive: %b", notificationMap.size(),
561                    getAttention, ignoreUnobtrusiveSetting);
562        }
563        // The number of unread messages for this account and label.
564        final Integer unread = notificationMap.getUnread(key);
565        final int unreadCount = unread != null ? unread.intValue() : 0;
566        final Integer unseen = notificationMap.getUnseen(key);
567        int unseenCount = unseen != null ? unseen.intValue() : 0;
568
569        Cursor cursor = null;
570
571        try {
572            final Uri.Builder uriBuilder = folder.conversationListUri.buildUpon();
573            uriBuilder.appendQueryParameter(
574                    UIProvider.SEEN_QUERY_PARAMETER, Boolean.FALSE.toString());
575            // Do not allow this quick check to disrupt any active network-enabled conversation
576            // cursor.
577            uriBuilder.appendQueryParameter(
578                    UIProvider.ConversationListQueryParameters.USE_NETWORK,
579                    Boolean.FALSE.toString());
580            cursor = context.getContentResolver().query(uriBuilder.build(),
581                    UIProvider.CONVERSATION_PROJECTION, null, null, null);
582            if (cursor == null) {
583                // This folder doesn't exist.
584                LogUtils.i(LOG_TAG,
585                        "The cursor is null, so the specified folder probably does not exist");
586                clearFolderNotification(context, account, folder, false);
587                return;
588            }
589            final int cursorUnseenCount = cursor.getCount();
590
591            // Make sure the unseen count matches the number of items in the cursor.  But, we don't
592            // want to overwrite a 0 unseen count that was specified in the intent
593            if (unseenCount != 0 && unseenCount != cursorUnseenCount) {
594                LogUtils.i(LOG_TAG,
595                        "Unseen count doesn't match cursor count.  unseen: %d cursor count: %d",
596                        unseenCount, cursorUnseenCount);
597                unseenCount = cursorUnseenCount;
598            }
599
600            // For the purpose of the notifications, the unseen count should be capped at the num of
601            // unread conversations.
602            if (unseenCount > unreadCount) {
603                unseenCount = unreadCount;
604            }
605
606            final int notificationId =
607                    getNotificationId(account.getAccountManagerAccount(), folder);
608
609            NotificationKey notificationKey = new NotificationKey(account, folder);
610
611            if (unseenCount == 0) {
612                LogUtils.i(LOG_TAG, "validateNotifications - cancelling account %s / folder %s",
613                        LogUtils.sanitizeName(LOG_TAG, account.getEmailAddress()),
614                        LogUtils.sanitizeName(LOG_TAG, folder.persistentId));
615                nm.cancel(notificationId);
616                cancelConversationNotifications(notificationKey, nm);
617
618                return;
619            }
620
621            // We now have all we need to create the notification and the pending intent
622            PendingIntent clickIntent = null;
623
624            NotificationCompat.Builder notification = new NotificationCompat.Builder(context);
625            NotificationCompat.WearableExtender wearableExtender =
626                    new NotificationCompat.WearableExtender();
627            Map<Integer, NotificationBuilders> msgNotifications =
628                    new ArrayMap<Integer, NotificationBuilders>();
629
630            if (com.android.mail.utils.Utils.isRunningLOrLater()) {
631                notification.setColor(
632                        context.getResources().getColor(R.color.notification_icon_color));
633            }
634
635            if(unseenCount > 1) {
636                notification.setSmallIcon(R.drawable.ic_notification_multiple_mail_24dp);
637            } else {
638                notification.setSmallIcon(R.drawable.ic_notification_mail_24dp);
639            }
640            notification.setTicker(account.getDisplayName());
641            notification.setVisibility(NotificationCompat.VISIBILITY_PRIVATE);
642            notification.setCategory(NotificationCompat.CATEGORY_EMAIL);
643
644            final long when;
645
646            final long oldWhen =
647                    NotificationActionUtils.sNotificationTimestamps.get(notificationId);
648            if (oldWhen != 0) {
649                when = oldWhen;
650            } else {
651                when = System.currentTimeMillis();
652            }
653
654            notification.setWhen(when);
655
656            // The timestamp is now stored in the notification, so we can remove it from here
657            NotificationActionUtils.sNotificationTimestamps.delete(notificationId);
658
659            // Dispatch a CLEAR_NEW_MAIL_NOTIFICATIONS intent if the user taps the "X" next to a
660            // notification.  Also this intent gets fired when the user taps on a notification as
661            // the AutoCancel flag has been set
662            final Intent cancelNotificationIntent =
663                    new Intent(MailIntentService.ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS);
664            cancelNotificationIntent.setPackage(context.getPackageName());
665            cancelNotificationIntent.setData(Utils.appendVersionQueryParameter(context,
666                    folder.folderUri.fullUri));
667            cancelNotificationIntent.putExtra(Utils.EXTRA_ACCOUNT, account);
668            cancelNotificationIntent.putExtra(Utils.EXTRA_FOLDER, folder);
669
670            notification.setDeleteIntent(PendingIntent.getService(
671                    context, notificationId, cancelNotificationIntent, 0));
672
673            // Ensure that the notification is cleared when the user selects it
674            notification.setAutoCancel(true);
675
676            boolean eventInfoConfigured = false;
677
678            final boolean isInbox = folder.folderUri.equals(account.settings.defaultInbox);
679            final FolderPreferences folderPreferences =
680                    new FolderPreferences(context, account.getAccountId(), folder, isInbox);
681
682            if (isInbox) {
683                final AccountPreferences accountPreferences =
684                        new AccountPreferences(context, account.getAccountId());
685                moveNotificationSetting(accountPreferences, folderPreferences);
686            }
687
688            if (!folderPreferences.areNotificationsEnabled()) {
689                LogUtils.i(LOG_TAG, "Notifications are disabled for this folder; not notifying");
690                // Don't notify
691                return;
692            }
693
694            if (unreadCount > 0) {
695                // How can I order this properly?
696                if (cursor.moveToNext()) {
697                    final Intent notificationIntent;
698
699                    // Launch directly to the conversation, if there is only 1 unseen conversation
700                    final boolean launchConversationMode = (unseenCount == 1);
701                    if (launchConversationMode) {
702                        notificationIntent = createViewConversationIntent(context, account, folder,
703                                cursor);
704                    } else {
705                        notificationIntent = createViewConversationIntent(context, account, folder,
706                                null);
707                    }
708
709                    Analytics.getInstance().sendEvent("notification_create",
710                            launchConversationMode ? "conversation" : "conversation_list",
711                            folder.getTypeDescription(), unseenCount);
712
713                    if (notificationIntent == null) {
714                        LogUtils.e(LOG_TAG, "Null intent when building notification");
715                        return;
716                    }
717
718                    clickIntent = createClickPendingIntent(context, notificationIntent);
719
720                    configureLatestEventInfoFromConversation(context, account, folderPreferences,
721                            notification, wearableExtender, msgNotifications, notificationId,
722                            cursor, clickIntent, notificationIntent, unreadCount, unseenCount,
723                            folder, when, contactFetcher);
724                    eventInfoConfigured = true;
725                }
726            }
727
728            final boolean vibrate = folderPreferences.isNotificationVibrateEnabled();
729            final String ringtoneUri = folderPreferences.getNotificationRingtoneUri();
730            final boolean notifyOnce = !folderPreferences.isEveryMessageNotificationEnabled();
731
732            if (!ignoreUnobtrusiveSetting && notifyOnce) {
733                // If the user has "unobtrusive notifications" enabled, only alert the first time
734                // new mail is received in this account.  This is the default behavior.  See
735                // bugs 2412348 and 2413490.
736                LogUtils.d(LOG_TAG, "Setting Alert Once");
737                notification.setOnlyAlertOnce(true);
738            }
739
740            LogUtils.i(LOG_TAG, "Account: %s vibrate: %s",
741                    LogUtils.sanitizeName(LOG_TAG, account.getEmailAddress()),
742                    Boolean.toString(folderPreferences.isNotificationVibrateEnabled()));
743
744            int defaults = 0;
745
746            // Check if any current conversation notifications exist previously.  Only notify if
747            // one of them is new.
748            boolean hasNewConversationNotification;
749            Set<Integer> prevConversationNotifications =
750                    sConversationNotificationMap.get(notificationKey);
751            if (prevConversationNotifications != null) {
752                hasNewConversationNotification = false;
753                for (Integer currentNotificationId : msgNotifications.keySet()) {
754                    if (!prevConversationNotifications.contains(currentNotificationId)) {
755                        hasNewConversationNotification = true;
756                        break;
757                    }
758                }
759            } else {
760                hasNewConversationNotification = true;
761            }
762
763            LogUtils.d(LOG_TAG, "getAttention=%s,oldWhen=%s,hasNewConversationNotification=%s",
764                    getAttention, oldWhen, hasNewConversationNotification);
765
766            /*
767             * We do not want to notify if this is coming back from an Undo notification, hence the
768             * oldWhen check.
769             */
770            if (getAttention && oldWhen == 0 && hasNewConversationNotification) {
771                final AccountPreferences accountPreferences =
772                        new AccountPreferences(context, account.getAccountId());
773                if (accountPreferences.areNotificationsEnabled()) {
774                    if (vibrate) {
775                        defaults |= Notification.DEFAULT_VIBRATE;
776                    }
777
778                    notification.setSound(TextUtils.isEmpty(ringtoneUri) ? null
779                            : Uri.parse(ringtoneUri));
780                    LogUtils.i(LOG_TAG, "New email in %s vibrateWhen: %s, playing notification: %s",
781                            LogUtils.sanitizeName(LOG_TAG, account.getEmailAddress()), vibrate,
782                            ringtoneUri);
783                }
784            }
785
786            // TODO(skennedy) Why do we do any of the above if we're just going to bail here?
787            if (eventInfoConfigured) {
788                defaults |= Notification.DEFAULT_LIGHTS;
789                notification.setDefaults(defaults);
790
791                if (oldWhen != 0) {
792                    // We do not want to display the ticker again if we are re-displaying this
793                    // notification (like from an Undo notification)
794                    notification.setTicker(null);
795                }
796
797                notification.extend(wearableExtender);
798
799                // create the *public* form of the *private* notification we have been assembling
800                final Notification publicNotification = createPublicNotification(context, account,
801                        folder, when, unseenCount, unreadCount, clickIntent);
802
803                notification.setPublicVersion(publicNotification);
804
805                nm.notify(notificationId, notification.build());
806
807                if (prevConversationNotifications != null) {
808                    Set<Integer> currentNotificationIds = msgNotifications.keySet();
809                    for (Integer prevConversationNotificationId : prevConversationNotifications) {
810                        if (!currentNotificationIds.contains(prevConversationNotificationId)) {
811                            nm.cancel(prevConversationNotificationId);
812                            LogUtils.d(LOG_TAG, "canceling conversation notification %s",
813                                    prevConversationNotificationId);
814                        }
815                    }
816                }
817
818                for (Map.Entry<Integer, NotificationBuilders> entry : msgNotifications.entrySet()) {
819                    NotificationBuilders builders = entry.getValue();
820                    builders.notifBuilder.extend(builders.wearableNotifBuilder);
821                    nm.notify(entry.getKey(), builders.notifBuilder.build());
822                    LogUtils.d(LOG_TAG, "notifying conversation notification %s", entry.getKey());
823                }
824
825                Set<Integer> conversationNotificationIds = new HashSet<Integer>();
826                conversationNotificationIds.addAll(msgNotifications.keySet());
827                sConversationNotificationMap.put(notificationKey, conversationNotificationIds);
828            } else {
829                LogUtils.i(LOG_TAG, "event info not configured - not notifying");
830            }
831        } finally {
832            if (cursor != null) {
833                cursor.close();
834            }
835        }
836    }
837
838    /**
839     * Build and return a redacted form of a notification using the given information. This redacted
840     * form is shown above the lock screen and is devoid of sensitive information.
841     *
842     * @param context a context used to construct the notification
843     * @param account the account for which the notification is being generated
844     * @param folder the folder for which the notification is being generated
845     * @param when the timestamp of the notification
846     * @param unseenCount the number of unseen messages
847     * @param unreadCount the number of unread messages
848     * @param clickIntent the behavior to invoke if the notification is tapped (note that the user
849     *                    will be prompted to unlock the device before the behavior is executed)
850     * @return the redacted form of the notification to display above the lock screen
851     */
852    private static Notification createPublicNotification(Context context, Account account,
853            Folder folder, long when, int unseenCount, int unreadCount, PendingIntent clickIntent) {
854        final boolean multipleUnseen = unseenCount > 1;
855
856        final NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
857                .setContentTitle(createTitle(context, unseenCount))
858                .setContentText(account.getDisplayName())
859                .setContentIntent(clickIntent)
860                .setNumber(unreadCount)
861                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
862                .setCategory(NotificationCompat.CATEGORY_EMAIL)
863                .setWhen(when);
864
865        if (com.android.mail.utils.Utils.isRunningLOrLater()) {
866            builder.setColor(context.getResources().getColor(R.color.notification_icon_color));
867        }
868
869        // if this public notification summarizes multiple single notifications, mark it as the
870        // summary notification and generate the same group key as the single notifications
871        if (multipleUnseen) {
872            builder.setGroup(createGroupKey(account, folder));
873            builder.setGroupSummary(true);
874            builder.setSmallIcon(R.drawable.ic_notification_multiple_mail_24dp);
875        } else {
876            builder.setSmallIcon(R.drawable.ic_notification_mail_24dp);
877        }
878
879        return builder.build();
880    }
881
882    /**
883     * @param account the account in which the unread email resides
884     * @param folder the folder in which the unread email resides
885     * @return a key that groups notifications with common accounts and folders
886     */
887    private static String createGroupKey(Account account, Folder folder) {
888        return account.uri.toString() + "/" + folder.folderUri.fullUri;
889    }
890
891    /**
892     * @param context a context used to construct the title
893     * @param unseenCount the number of unseen messages
894     * @return e.g. "1 new message" or "2 new messages"
895     */
896    private static String createTitle(Context context, int unseenCount) {
897        final Resources resources = context.getResources();
898        return resources.getQuantityString(R.plurals.new_messages, unseenCount, unseenCount);
899    }
900
901    private static PendingIntent createClickPendingIntent(Context context,
902            Intent notificationIntent) {
903        // Amend the click intent with a hint that its source was a notification,
904        // but remove the hint before it's used to generate notification action
905        // intents. This prevents the following sequence:
906        // 1. generate single notification
907        // 2. user clicks reply, then completes Compose activity
908        // 3. main activity launches, gets FROM_NOTIFICATION hint in intent
909        notificationIntent.putExtra(Utils.EXTRA_FROM_NOTIFICATION, true);
910        PendingIntent clickIntent = PendingIntent.getActivity(context, -1, notificationIntent,
911                PendingIntent.FLAG_UPDATE_CURRENT);
912        notificationIntent.removeExtra(Utils.EXTRA_FROM_NOTIFICATION);
913        return clickIntent;
914    }
915
916    /**
917     * @return an {@link Intent} which, if launched, will display the corresponding conversation
918     */
919    private static Intent createViewConversationIntent(final Context context, final Account account,
920            final Folder folder, final Cursor cursor) {
921        if (folder == null || account == null) {
922            LogUtils.e(LOG_TAG, "createViewConversationIntent(): "
923                    + "Null account or folder.  account: %s folder: %s", account, folder);
924            return null;
925        }
926
927        final Intent intent;
928
929        if (cursor == null) {
930            intent = Utils.createViewFolderIntent(context, folder.folderUri.fullUri, account);
931        } else {
932            // A conversation cursor has been specified, so this intent is intended to be go
933            // directly to the one new conversation
934
935            // Get the Conversation object
936            final Conversation conversation = new Conversation(cursor);
937            intent = Utils.createViewConversationIntent(context, conversation,
938                    folder.folderUri.fullUri, account);
939        }
940
941        return intent;
942    }
943
944    private static Bitmap getIcon(final Context context, final int resId) {
945        final Bitmap cachedIcon = sNotificationIcons.get(resId);
946        if (cachedIcon != null) {
947            return cachedIcon;
948        }
949
950        final Bitmap icon = BitmapFactory.decodeResource(context.getResources(), resId);
951        sNotificationIcons.put(resId, icon);
952
953        return icon;
954    }
955
956    private static Bitmap getDefaultWearableBg(Context context) {
957        Bitmap bg = sDefaultWearableBg.get();
958        if (bg == null) {
959            bg = BitmapFactory.decodeResource(context.getResources(), R.drawable.bg_email);
960            sDefaultWearableBg = new WeakReference<>(bg);
961        }
962        return bg;
963    }
964
965    private static void configureLatestEventInfoFromConversation(final Context context,
966            final Account account, final FolderPreferences folderPreferences,
967            final NotificationCompat.Builder notificationBuilder,
968            final NotificationCompat.WearableExtender wearableExtender,
969            final Map<Integer, NotificationBuilders> msgNotifications,
970            final int summaryNotificationId, final Cursor conversationCursor,
971            final PendingIntent clickIntent, final Intent notificationIntent,
972            final int unreadCount, final int unseenCount,
973            final Folder folder, final long when, final ContactFetcher contactFetcher) {
974        final Resources res = context.getResources();
975        final boolean multipleUnseen = unseenCount > 1;
976
977        LogUtils.i(LOG_TAG, "Showing notification with unreadCount of %d and unseenCount of %d",
978                unreadCount, unseenCount);
979
980        String notificationTicker = null;
981
982        // Boolean indicating that this notification is for a non-inbox label.
983        final boolean isInbox = folder.folderUri.fullUri.equals(account.settings.defaultInbox);
984
985        // Notification label name for user label notifications.
986        final String notificationLabelName = isInbox ? null : folder.name;
987
988        if (multipleUnseen) {
989            // Build the string that describes the number of new messages
990            final String newMessagesString = createTitle(context, unseenCount);
991
992            // The ticker initially start as the new messages string.
993            notificationTicker = newMessagesString;
994
995            // The title of the notification is the new messages string
996            notificationBuilder.setContentTitle(newMessagesString);
997
998            // TODO(skennedy) Can we remove this check?
999            if (com.android.mail.utils.Utils.isRunningJellybeanOrLater()) {
1000                // For a new-style notification
1001                final int maxNumDigestItems = context.getResources().getInteger(
1002                        R.integer.max_num_notification_digest_items);
1003
1004                // The body of the notification is the account name, or the label name.
1005                notificationBuilder.setSubText(
1006                        isInbox ? account.getDisplayName() : notificationLabelName);
1007
1008                final NotificationCompat.InboxStyle digest =
1009                        new NotificationCompat.InboxStyle(notificationBuilder);
1010
1011                // Group by account and folder
1012                final String notificationGroupKey = createGroupKey(account, folder);
1013                // Track all senders to later tag them along with the digest notification
1014                final HashSet<String> senderAddressesSet = new HashSet<String>();
1015                notificationBuilder.setGroup(notificationGroupKey).setGroupSummary(true);
1016
1017                ConfigResult firstResult = null;
1018                int numDigestItems = 0;
1019                do {
1020                    final Conversation conversation = new Conversation(conversationCursor);
1021
1022                    if (!conversation.read) {
1023                        boolean multipleUnreadThread = false;
1024                        // TODO(cwren) extract this pattern into a helper
1025
1026                        Cursor cursor = null;
1027                        MessageCursor messageCursor = null;
1028                        try {
1029                            final Uri.Builder uriBuilder = conversation.messageListUri.buildUpon();
1030                            uriBuilder.appendQueryParameter(
1031                                    UIProvider.LABEL_QUERY_PARAMETER, notificationLabelName);
1032                            cursor = context.getContentResolver().query(uriBuilder.build(),
1033                                    UIProvider.MESSAGE_PROJECTION, null, null, null);
1034                            messageCursor = new MessageCursor(cursor);
1035
1036                            String from = "";
1037                            String fromAddress = "";
1038                            if (messageCursor.moveToPosition(messageCursor.getCount() - 1)) {
1039                                final Message message = messageCursor.getMessage();
1040                                fromAddress = message.getFrom();
1041                                if (fromAddress == null) {
1042                                    fromAddress = "";
1043                                }
1044                                from = getDisplayableSender(fromAddress);
1045                                addEmailAddressToSet(fromAddress, senderAddressesSet);
1046                            }
1047                            while (messageCursor.moveToPosition(messageCursor.getPosition() - 1)) {
1048                                final Message message = messageCursor.getMessage();
1049                                if (!message.read &&
1050                                        !fromAddress.contentEquals(message.getFrom())) {
1051                                    multipleUnreadThread = true;
1052                                    addEmailAddressToSet(message.getFrom(), senderAddressesSet);
1053                                }
1054                            }
1055                            final SpannableStringBuilder sendersBuilder;
1056                            if (multipleUnreadThread) {
1057                                final int sendersLength =
1058                                        res.getInteger(R.integer.swipe_senders_length);
1059
1060                                sendersBuilder = getStyledSenders(context, conversationCursor,
1061                                        sendersLength, account);
1062                            } else {
1063                                sendersBuilder =
1064                                        new SpannableStringBuilder(getWrappedFromString(from));
1065                            }
1066                            final CharSequence digestLine = getSingleMessageInboxLine(context,
1067                                    sendersBuilder.toString(),
1068                                    ConversationItemView.filterTag(context, conversation.subject),
1069                                    conversation.getSnippet());
1070                            digest.addLine(digestLine);
1071                            numDigestItems++;
1072
1073                            // Adding conversation notification for Wear.
1074                            NotificationCompat.Builder conversationNotif =
1075                                    new NotificationCompat.Builder(context);
1076                            conversationNotif.setCategory(NotificationCompat.CATEGORY_EMAIL);
1077
1078                            conversationNotif.setSmallIcon(
1079                                    R.drawable.ic_notification_multiple_mail_24dp);
1080
1081                            if (com.android.mail.utils.Utils.isRunningLOrLater()) {
1082                                conversationNotif.setColor(
1083                                        context.getResources()
1084                                                .getColor(R.color.notification_icon_color));
1085                            }
1086                            conversationNotif.setContentText(digestLine);
1087                            Intent conversationNotificationIntent = createViewConversationIntent(
1088                                    context, account, folder, conversationCursor);
1089                            PendingIntent conversationClickIntent = createClickPendingIntent(
1090                                    context, conversationNotificationIntent);
1091                            conversationNotif.setContentIntent(conversationClickIntent);
1092                            conversationNotif.setAutoCancel(true);
1093
1094                            // Conversations are sorted in descending order, but notification sort
1095                            // key is in ascending order.  Invert the order key to get the right
1096                            // order.  Left pad 19 zeros because it's a long.
1097                            String groupSortKey = String.format("%019d",
1098                                    (Long.MAX_VALUE - conversation.orderKey));
1099                            conversationNotif.setGroup(notificationGroupKey);
1100                            conversationNotif.setSortKey(groupSortKey);
1101                            conversationNotif.setWhen(conversation.dateMs);
1102
1103                            int conversationNotificationId = getNotificationId(
1104                                    summaryNotificationId, conversation.hashCode());
1105
1106                            final NotificationCompat.WearableExtender conversationWearExtender =
1107                                    new NotificationCompat.WearableExtender();
1108                            final ConfigResult result =
1109                                    configureNotifForOneConversation(context, account,
1110                                    folderPreferences, conversationNotif, conversationWearExtender,
1111                                    conversationCursor, notificationIntent, folder, when, res,
1112                                    isInbox, notificationLabelName, conversationNotificationId,
1113                                    contactFetcher);
1114                            msgNotifications.put(conversationNotificationId,
1115                                    NotificationBuilders.of(conversationNotif,
1116                                            conversationWearExtender));
1117
1118                            if (firstResult == null) {
1119                                firstResult = result;
1120                            }
1121                        } finally {
1122                            if (messageCursor != null) {
1123                                messageCursor.close();
1124                            }
1125                            if (cursor != null) {
1126                                cursor.close();
1127                            }
1128                        }
1129                    }
1130                } while (numDigestItems <= maxNumDigestItems && conversationCursor.moveToNext());
1131
1132                // Tag main digest notification with the senders
1133                tagNotificationsWithPeople(notificationBuilder, senderAddressesSet);
1134
1135                if (firstResult != null && firstResult.contactIconInfo != null) {
1136                    wearableExtender.setBackground(firstResult.contactIconInfo.wearableBg);
1137                } else {
1138                    LogUtils.w(LOG_TAG, "First contact icon is null!");
1139                    wearableExtender.setBackground(getDefaultWearableBg(context));
1140                }
1141            } else {
1142                // The body of the notification is the account name, or the label name.
1143                notificationBuilder.setContentText(
1144                        isInbox ? account.getDisplayName() : notificationLabelName);
1145            }
1146        } else {
1147            // For notifications for a single new conversation, we want to get the information
1148            // from the conversation
1149
1150            // Move the cursor to the most recent unread conversation
1151            seekToLatestUnreadConversation(conversationCursor);
1152
1153            final ConfigResult result = configureNotifForOneConversation(context, account,
1154                    folderPreferences, notificationBuilder, wearableExtender, conversationCursor,
1155                    notificationIntent, folder, when, res, isInbox, notificationLabelName,
1156                    summaryNotificationId, contactFetcher);
1157            notificationTicker = result.notificationTicker;
1158
1159            if (result.contactIconInfo != null) {
1160                wearableExtender.setBackground(result.contactIconInfo.wearableBg);
1161            } else {
1162                wearableExtender.setBackground(getDefaultWearableBg(context));
1163            }
1164        }
1165
1166        // Build the notification ticker
1167        if (notificationLabelName != null && notificationTicker != null) {
1168            // This is a per label notification, format the ticker with that information
1169            notificationTicker = res.getString(R.string.label_notification_ticker,
1170                    notificationLabelName, notificationTicker);
1171        }
1172
1173        if (notificationTicker != null) {
1174            // If we didn't generate a notification ticker, it will default to account name
1175            notificationBuilder.setTicker(notificationTicker);
1176        }
1177
1178        // Set the number in the notification
1179        if (unreadCount > 1) {
1180            notificationBuilder.setNumber(unreadCount);
1181        }
1182
1183        notificationBuilder.setContentIntent(clickIntent);
1184    }
1185
1186    /**
1187     * Configure the notification for one conversation.  When there are multiple conversations,
1188     * this method is used to configure bundled notification for Android Wear.
1189     */
1190    private static ConfigResult configureNotifForOneConversation(Context context,
1191            Account account, FolderPreferences folderPreferences,
1192            NotificationCompat.Builder notificationBuilder,
1193            NotificationCompat.WearableExtender wearExtender, Cursor conversationCursor,
1194            Intent notificationIntent, Folder folder, long when, Resources res,
1195            boolean isInbox, String notificationLabelName, int notificationId,
1196            final ContactFetcher contactFetcher) {
1197
1198        final ConfigResult result = new ConfigResult();
1199
1200        final Conversation conversation = new Conversation(conversationCursor);
1201
1202        // Set of all unique senders for unseen messages
1203        final HashSet<String> senderAddressesSet = new HashSet<String>();
1204        Cursor cursor = null;
1205        MessageCursor messageCursor = null;
1206        boolean multipleUnseenThread = false;
1207        String from = null;
1208        try {
1209            final Uri uri = conversation.messageListUri.buildUpon().appendQueryParameter(
1210                    UIProvider.LABEL_QUERY_PARAMETER, folder.persistentId).build();
1211            cursor = context.getContentResolver().query(uri, UIProvider.MESSAGE_PROJECTION,
1212                    null, null, null);
1213            messageCursor = new MessageCursor(cursor);
1214            // Use the information from the last sender in the conversation that triggered
1215            // this notification.
1216
1217            String fromAddress = "";
1218            if (messageCursor.moveToPosition(messageCursor.getCount() - 1)) {
1219                final Message message = messageCursor.getMessage();
1220                fromAddress = message.getFrom();
1221                if (fromAddress == null) {
1222                    // No sender. Go back to default value.
1223                    LogUtils.e(LOG_TAG, "No sender found for message: %d", message.getId());
1224                    fromAddress = "";
1225                }
1226                from = getDisplayableSender(fromAddress);
1227                result.contactIconInfo = getContactIcon(
1228                        context, account.getAccountManagerAccount().name, from,
1229                        getSenderAddress(fromAddress), folder, contactFetcher);
1230                addEmailAddressToSet(fromAddress, senderAddressesSet);
1231                notificationBuilder.setLargeIcon(result.contactIconInfo.icon);
1232            }
1233
1234            // Assume that the last message in this conversation is unread
1235            int firstUnseenMessagePos = messageCursor.getPosition();
1236            while (messageCursor.moveToPosition(messageCursor.getPosition() - 1)) {
1237                final Message message = messageCursor.getMessage();
1238                final boolean unseen = !message.seen;
1239                if (unseen) {
1240                    firstUnseenMessagePos = messageCursor.getPosition();
1241                    addEmailAddressToSet(message.getFrom(), senderAddressesSet);
1242                    if (!multipleUnseenThread
1243                            && !fromAddress.contentEquals(message.getFrom())) {
1244                        multipleUnseenThread = true;
1245                    }
1246                }
1247            }
1248
1249            final String subject = ConversationItemView.filterTag(context, conversation.subject);
1250
1251            // TODO(skennedy) Can we remove this check?
1252            if (Utils.isRunningJellybeanOrLater()) {
1253                // For a new-style notification
1254
1255                if (multipleUnseenThread) {
1256                    // The title of a single conversation is the list of senders.
1257                    int sendersLength = res.getInteger(R.integer.swipe_senders_length);
1258
1259                    final SpannableStringBuilder sendersBuilder = getStyledSenders(
1260                            context, conversationCursor, sendersLength, account);
1261
1262                    notificationBuilder.setContentTitle(sendersBuilder);
1263                    // For a single new conversation, the ticker is based on the sender's name.
1264                    result.notificationTicker = sendersBuilder.toString();
1265                } else {
1266                    from = getWrappedFromString(from);
1267                    // The title of a single message the sender.
1268                    notificationBuilder.setContentTitle(from);
1269                    // For a single new conversation, the ticker is based on the sender's name.
1270                    result.notificationTicker = from;
1271                }
1272
1273                // The notification content will be the subject of the conversation.
1274                notificationBuilder.setContentText(getSingleMessageLittleText(context, subject));
1275
1276                // The notification subtext will be the subject of the conversation for inbox
1277                // notifications, or will based on the the label name for user label
1278                // notifications.
1279                notificationBuilder.setSubText(isInbox ?
1280                        account.getDisplayName() : notificationLabelName);
1281
1282                final NotificationCompat.BigTextStyle bigText =
1283                        new NotificationCompat.BigTextStyle(notificationBuilder);
1284
1285                // Seek the message cursor to the first unread message
1286                final Message message;
1287                if (messageCursor.moveToPosition(firstUnseenMessagePos)) {
1288                    message = messageCursor.getMessage();
1289                    bigText.bigText(getSingleMessageBigText(context, subject, message));
1290                } else {
1291                    LogUtils.e(LOG_TAG, "Failed to load message");
1292                    message = null;
1293                }
1294
1295                if (message != null) {
1296                    final Set<String> notificationActions =
1297                            folderPreferences.getNotificationActions(account);
1298
1299                    NotificationActionUtils.addNotificationActions(context, notificationIntent,
1300                            notificationBuilder, wearExtender, account, conversation, message,
1301                            folder, notificationId, when, notificationActions);
1302                }
1303            } else {
1304                // For an old-style notification
1305
1306                // The title of a single conversation notification is built from both the sender
1307                // and subject of the new message.
1308                notificationBuilder.setContentTitle(
1309                        getSingleMessageNotificationTitle(context, from, subject));
1310
1311                // The notification content will be the subject of the conversation for inbox
1312                // notifications, or will based on the the label name for user label
1313                // notifications.
1314                notificationBuilder.setContentText(
1315                        isInbox ? account.getDisplayName() : notificationLabelName);
1316
1317                // For a single new conversation, the ticker is based on the sender's name.
1318                result.notificationTicker = from;
1319            }
1320
1321            tagNotificationsWithPeople(notificationBuilder, senderAddressesSet);
1322        } finally {
1323            if (messageCursor != null) {
1324                messageCursor.close();
1325            }
1326            if (cursor != null) {
1327                cursor.close();
1328            }
1329        }
1330        return result;
1331    }
1332
1333    /**
1334     * Iterates through all senders and adds their respective Uris to the notifications. Each Uri
1335     * string consists of the prefix "mailto:" followed by the sender address.
1336     * @param notificationBuilder
1337     * @param senderAddressesSet List of unique senders to be tagged with the conversation
1338     */
1339    private static void tagNotificationsWithPeople(NotificationCompat.Builder notificationBuilder,
1340            HashSet<String> senderAddressesSet) {
1341        for (final String sender : senderAddressesSet) {
1342            if (TextUtils.isEmpty(sender)) {
1343                continue;
1344            }
1345            // Tag a notification with a person using "mailto:<sender address>"
1346            notificationBuilder.addPerson(MailTo.MAILTO_SCHEME.concat(sender));
1347        }
1348    }
1349
1350    private static String getWrappedFromString(String from) {
1351        if (from == null) {
1352            LogUtils.e(LOG_TAG, "null from string in getWrappedFromString");
1353            from = "";
1354        }
1355        from = sBidiFormatter.unicodeWrap(from);
1356        return from;
1357    }
1358
1359    private static SpannableStringBuilder getStyledSenders(final Context context,
1360            final Cursor conversationCursor, final int maxLength, final Account account) {
1361        final Conversation conversation = new Conversation(conversationCursor);
1362        final com.android.mail.providers.ConversationInfo conversationInfo =
1363                conversation.conversationInfo;
1364        final ArrayList<SpannableString> senders = new ArrayList<>();
1365        if (sNotificationUnreadStyleSpan == null) {
1366            sNotificationUnreadStyleSpan = new TextAppearanceSpan(
1367                    context, R.style.NotificationSendersUnreadTextAppearance);
1368            sNotificationReadStyleSpan =
1369                    new TextAppearanceSpan(context, R.style.NotificationSendersReadTextAppearance);
1370        }
1371        SendersView.format(context, conversationInfo, "", maxLength, senders, null, null, account,
1372                sNotificationUnreadStyleSpan, sNotificationReadStyleSpan,
1373                false /* showToHeader */, false /* resourceCachingRequired */);
1374
1375        return ellipsizeStyledSenders(context, senders);
1376    }
1377
1378    private static String sSendersSplitToken = null;
1379    private static String sElidedPaddingToken = null;
1380
1381    private static SpannableStringBuilder ellipsizeStyledSenders(final Context context,
1382            ArrayList<SpannableString> styledSenders) {
1383        if (sSendersSplitToken == null) {
1384            sSendersSplitToken = context.getString(R.string.senders_split_token);
1385            sElidedPaddingToken = context.getString(R.string.elided_padding_token);
1386        }
1387
1388        SpannableStringBuilder builder = new SpannableStringBuilder();
1389        SpannableString prevSender = null;
1390        for (SpannableString sender : styledSenders) {
1391            if (sender == null) {
1392                LogUtils.e(LOG_TAG, "null sender iterating over styledSenders");
1393                continue;
1394            }
1395            CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class);
1396            if (SendersView.sElidedString.equals(sender.toString())) {
1397                prevSender = sender;
1398                sender = copyStyles(spans, sElidedPaddingToken + sender + sElidedPaddingToken);
1399            } else if (builder.length() > 0
1400                    && (prevSender == null || !SendersView.sElidedString.equals(prevSender
1401                            .toString()))) {
1402                prevSender = sender;
1403                sender = copyStyles(spans, sSendersSplitToken + sender);
1404            } else {
1405                prevSender = sender;
1406            }
1407            builder.append(sender);
1408        }
1409        return builder;
1410    }
1411
1412    private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) {
1413        SpannableString s = new SpannableString(newText);
1414        if (spans != null && spans.length > 0) {
1415            s.setSpan(spans[0], 0, s.length(), 0);
1416        }
1417        return s;
1418    }
1419
1420    /**
1421     * Seeks the cursor to the position of the most recent unread conversation. If no unread
1422     * conversation is found, the position of the cursor will be restored, and false will be
1423     * returned.
1424     */
1425    private static boolean seekToLatestUnreadConversation(final Cursor cursor) {
1426        final int initialPosition = cursor.getPosition();
1427        do {
1428            final Conversation conversation = new Conversation(cursor);
1429            if (!conversation.read) {
1430                return true;
1431            }
1432        } while (cursor.moveToNext());
1433
1434        // Didn't find an unread conversation, reset the position.
1435        cursor.moveToPosition(initialPosition);
1436        return false;
1437    }
1438
1439    /**
1440     * Sets the bigtext for a notification for a single new conversation
1441     *
1442     * @param context
1443     * @param senders Sender of the new message that triggered the notification.
1444     * @param subject Subject of the new message that triggered the notification
1445     * @param snippet Snippet of the new message that triggered the notification
1446     * @return a {@link CharSequence} suitable for use in
1447     *         {@link android.support.v4.app.NotificationCompat.BigTextStyle}
1448     */
1449    private static CharSequence getSingleMessageInboxLine(Context context,
1450            String senders, String subject, String snippet) {
1451        // TODO(cwren) finish this step toward commmon code with getSingleMessageBigText
1452
1453        final String subjectSnippet = !TextUtils.isEmpty(subject) ? subject : snippet;
1454
1455        final TextAppearanceSpan notificationPrimarySpan =
1456                new TextAppearanceSpan(context, R.style.NotificationPrimaryText);
1457
1458        if (TextUtils.isEmpty(senders)) {
1459            // If the senders are empty, just use the subject/snippet.
1460            return subjectSnippet;
1461        } else if (TextUtils.isEmpty(subjectSnippet)) {
1462            // If the subject/snippet is empty, just use the senders.
1463            final SpannableString spannableString = new SpannableString(senders);
1464            spannableString.setSpan(notificationPrimarySpan, 0, senders.length(), 0);
1465
1466            return spannableString;
1467        } else {
1468            final String formatString = context.getResources().getString(
1469                    R.string.multiple_new_message_notification_item);
1470            final TextAppearanceSpan notificationSecondarySpan =
1471                    new TextAppearanceSpan(context, R.style.NotificationSecondaryText);
1472
1473            // senders is already individually unicode wrapped so it does not need to be done here
1474            final String instantiatedString = String.format(formatString,
1475                    senders,
1476                    sBidiFormatter.unicodeWrap(subjectSnippet));
1477
1478            final SpannableString spannableString = new SpannableString(instantiatedString);
1479
1480            final boolean isOrderReversed = formatString.indexOf("%2$s") <
1481                    formatString.indexOf("%1$s");
1482            final int primaryOffset =
1483                    (isOrderReversed ? instantiatedString.lastIndexOf(senders) :
1484                     instantiatedString.indexOf(senders));
1485            final int secondaryOffset =
1486                    (isOrderReversed ? instantiatedString.lastIndexOf(subjectSnippet) :
1487                     instantiatedString.indexOf(subjectSnippet));
1488            spannableString.setSpan(notificationPrimarySpan,
1489                    primaryOffset, primaryOffset + senders.length(), 0);
1490            spannableString.setSpan(notificationSecondarySpan,
1491                    secondaryOffset, secondaryOffset + subjectSnippet.length(), 0);
1492            return spannableString;
1493        }
1494    }
1495
1496    /**
1497     * Sets the bigtext for a notification for a single new conversation
1498     * @param context
1499     * @param subject Subject of the new message that triggered the notification
1500     * @return a {@link CharSequence} suitable for use in
1501     * {@link NotificationCompat.Builder#setContentText}
1502     */
1503    private static CharSequence getSingleMessageLittleText(Context context, String subject) {
1504        final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
1505                context, R.style.NotificationPrimaryText);
1506
1507        final SpannableString spannableString = new SpannableString(subject);
1508        spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0);
1509
1510        return spannableString;
1511    }
1512
1513    /**
1514     * Sets the bigtext for a notification for a single new conversation
1515     *
1516     * @param context
1517     * @param subject Subject of the new message that triggered the notification
1518     * @param message the {@link Message} to be displayed.
1519     * @return a {@link CharSequence} suitable for use in
1520     *         {@link android.support.v4.app.NotificationCompat.BigTextStyle}
1521     */
1522    private static CharSequence getSingleMessageBigText(Context context, String subject,
1523            final Message message) {
1524
1525        final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
1526                context, R.style.NotificationPrimaryText);
1527
1528        final String snippet = getMessageBodyWithoutElidedText(message);
1529
1530        // Change multiple newlines (with potential white space between), into a single new line
1531        final String collapsedSnippet =
1532                !TextUtils.isEmpty(snippet) ? snippet.replaceAll("\\n\\s+", "\n") : "";
1533
1534        if (TextUtils.isEmpty(subject)) {
1535            // If the subject is empty, just use the snippet.
1536            return snippet;
1537        } else if (TextUtils.isEmpty(collapsedSnippet)) {
1538            // If the snippet is empty, just use the subject.
1539            final SpannableString spannableString = new SpannableString(subject);
1540            spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0);
1541
1542            return spannableString;
1543        } else {
1544            final String notificationBigTextFormat = context.getResources().getString(
1545                    R.string.single_new_message_notification_big_text);
1546
1547            // Localizers may change the order of the parameters, look at how the format
1548            // string is structured.
1549            final boolean isSubjectFirst = notificationBigTextFormat.indexOf("%2$s") >
1550                    notificationBigTextFormat.indexOf("%1$s");
1551            final String bigText =
1552                    String.format(notificationBigTextFormat, subject, collapsedSnippet);
1553            final SpannableString spannableString = new SpannableString(bigText);
1554
1555            final int subjectOffset =
1556                    (isSubjectFirst ? bigText.indexOf(subject) : bigText.lastIndexOf(subject));
1557            spannableString.setSpan(notificationSubjectSpan,
1558                    subjectOffset, subjectOffset + subject.length(), 0);
1559
1560            return spannableString;
1561        }
1562    }
1563
1564    /**
1565     * Gets the title for a notification for a single new conversation
1566     * @param context
1567     * @param sender Sender of the new message that triggered the notification.
1568     * @param subject Subject of the new message that triggered the notification
1569     * @return a {@link CharSequence} suitable for use as a {@link Notification} title.
1570     */
1571    private static CharSequence getSingleMessageNotificationTitle(Context context,
1572            String sender, String subject) {
1573
1574        if (TextUtils.isEmpty(subject)) {
1575            // If the subject is empty, just set the title to the sender's information.
1576            return sender;
1577        } else {
1578            final String notificationTitleFormat = context.getResources().getString(
1579                    R.string.single_new_message_notification_title);
1580
1581            // Localizers may change the order of the parameters, look at how the format
1582            // string is structured.
1583            final boolean isSubjectLast = notificationTitleFormat.indexOf("%2$s") >
1584                    notificationTitleFormat.indexOf("%1$s");
1585            final String titleString = String.format(notificationTitleFormat, sender, subject);
1586
1587            // Format the string so the subject is using the secondaryText style
1588            final SpannableString titleSpannable = new SpannableString(titleString);
1589
1590            // Find the offset of the subject.
1591            final int subjectOffset =
1592                    isSubjectLast ? titleString.lastIndexOf(subject) : titleString.indexOf(subject);
1593            final TextAppearanceSpan notificationSubjectSpan =
1594                    new TextAppearanceSpan(context, R.style.NotificationSecondaryText);
1595            titleSpannable.setSpan(notificationSubjectSpan,
1596                    subjectOffset, subjectOffset + subject.length(), 0);
1597            return titleSpannable;
1598        }
1599    }
1600
1601    /**
1602     * Clears the notifications for the specified account/folder.
1603     */
1604    public static void clearFolderNotification(Context context, Account account, Folder folder,
1605            final boolean markSeen) {
1606        LogUtils.v(LOG_TAG, "Clearing all notifications for %s/%s", account.getEmailAddress(),
1607                folder.name);
1608        final NotificationMap notificationMap = getNotificationMap(context);
1609        final NotificationKey key = new NotificationKey(account, folder);
1610        notificationMap.remove(key);
1611        notificationMap.saveNotificationMap(context);
1612
1613        final NotificationManagerCompat notificationManager =
1614                NotificationManagerCompat.from(context);
1615        notificationManager.cancel(getNotificationId(account.getAccountManagerAccount(), folder));
1616
1617        cancelConversationNotifications(key, notificationManager);
1618
1619        if (markSeen) {
1620            markSeen(context, folder);
1621        }
1622    }
1623
1624    /**
1625     * Use content resolver to update a conversation.  Should not be called from a main thread.
1626     */
1627    public static void markConversationAsReadAndSeen(Context context, Uri conversationUri) {
1628        LogUtils.v(LOG_TAG, "markConversationAsReadAndSeen=%s", conversationUri);
1629
1630        final ContentValues values = new ContentValues(2);
1631        values.put(UIProvider.ConversationColumns.SEEN, Boolean.TRUE);
1632        values.put(UIProvider.ConversationColumns.READ, Boolean.TRUE);
1633        context.getContentResolver().update(conversationUri, values, null, null);
1634    }
1635
1636    /**
1637     * Clears all notifications for the specified account.
1638     */
1639    public static void clearAccountNotifications(final Context context,
1640            final android.accounts.Account account) {
1641        LogUtils.v(LOG_TAG, "Clearing all notifications for %s", account);
1642        final NotificationMap notificationMap = getNotificationMap(context);
1643
1644        // Find all NotificationKeys for this account
1645        final ImmutableList.Builder<NotificationKey> keyBuilder = ImmutableList.builder();
1646
1647        for (final NotificationKey key : notificationMap.keySet()) {
1648            if (account.equals(key.account.getAccountManagerAccount())) {
1649                keyBuilder.add(key);
1650            }
1651        }
1652
1653        final List<NotificationKey> notificationKeys = keyBuilder.build();
1654
1655        final NotificationManagerCompat notificationManager =
1656                NotificationManagerCompat.from(context);
1657
1658        for (final NotificationKey notificationKey : notificationKeys) {
1659            final Folder folder = notificationKey.folder;
1660            notificationManager.cancel(getNotificationId(account, folder));
1661            notificationMap.remove(notificationKey);
1662
1663            cancelConversationNotifications(notificationKey, notificationManager);
1664        }
1665
1666        notificationMap.saveNotificationMap(context);
1667    }
1668
1669    private static void cancelConversationNotifications(NotificationKey key,
1670            NotificationManagerCompat nm) {
1671        final Set<Integer> conversationNotifications = sConversationNotificationMap.get(key);
1672        if (conversationNotifications != null) {
1673            for (Integer conversationNotification : conversationNotifications) {
1674                nm.cancel(conversationNotification);
1675            }
1676            sConversationNotificationMap.remove(key);
1677        }
1678    }
1679
1680    private static ContactIconInfo getContactIcon(final Context context, String accountName,
1681            final String displayName, final String senderAddress, final Folder folder,
1682            final ContactFetcher contactFetcher) {
1683        if (Looper.myLooper() == Looper.getMainLooper()) {
1684            throw new IllegalStateException(
1685                    "getContactIcon should not be called on the main thread.");
1686        }
1687
1688        final ContactIconInfo contactIconInfo;
1689        if (TextUtils.isEmpty(senderAddress)) {
1690            contactIconInfo = new ContactIconInfo();
1691        } else {
1692            // Get the ideal size for this icon.
1693            final Resources res = context.getResources();
1694            final int idealIconHeight =
1695                    res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
1696            final int idealIconWidth =
1697                    res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
1698            final int idealWearableBgWidth =
1699                    res.getDimensionPixelSize(R.dimen.wearable_background_width);
1700            final int idealWearableBgHeight =
1701                    res.getDimensionPixelSize(R.dimen.wearable_background_height);
1702
1703            if (contactFetcher != null) {
1704                contactIconInfo = contactFetcher.getContactPhoto(context, accountName,
1705                        senderAddress, idealIconWidth, idealIconHeight, idealWearableBgWidth,
1706                        idealWearableBgHeight);
1707            } else {
1708                contactIconInfo = getContactInfo(context, senderAddress, idealIconWidth,
1709                        idealIconHeight, idealWearableBgWidth, idealWearableBgHeight);
1710            }
1711
1712            if (contactIconInfo.icon == null) {
1713                // Make a colorful tile!
1714                final Dimensions dimensions = new Dimensions(idealIconWidth, idealIconHeight,
1715                        Dimensions.SCALE_ONE);
1716
1717                contactIconInfo.icon = new LetterTileProvider(context.getResources())
1718                        .getLetterTile(dimensions, displayName, senderAddress);
1719            }
1720
1721            // Only turn the square photo/letter tile into a circle for L and later
1722            if (Utils.isRunningLOrLater()) {
1723                contactIconInfo.icon = BitmapUtil.frameBitmapInCircle(contactIconInfo.icon);
1724            }
1725        }
1726
1727        if (contactIconInfo.icon == null) {
1728            // Use anonymous icon due to lack of sender
1729            contactIconInfo.icon = getIcon(context,
1730                    R.drawable.ic_notification_anonymous_avatar_32dp);
1731        }
1732
1733        if (contactIconInfo.wearableBg == null) {
1734            contactIconInfo.wearableBg = getDefaultWearableBg(context);
1735        }
1736
1737        return contactIconInfo;
1738    }
1739
1740    private static ArrayList<Long> findContacts(Context context, Collection<String> addresses) {
1741        ArrayList<String> whereArgs = new ArrayList<String>();
1742        StringBuilder whereBuilder = new StringBuilder();
1743        String[] questionMarks = new String[addresses.size()];
1744
1745        whereArgs.addAll(addresses);
1746        Arrays.fill(questionMarks, "?");
1747        whereBuilder.append(Email.DATA1 + " IN (").
1748                append(TextUtils.join(",", questionMarks)).
1749                append(")");
1750
1751        ContentResolver resolver = context.getContentResolver();
1752        Cursor c = resolver.query(Email.CONTENT_URI,
1753                new String[] {Email.CONTACT_ID}, whereBuilder.toString(),
1754                whereArgs.toArray(new String[0]), null);
1755
1756        ArrayList<Long> contactIds = new ArrayList<Long>();
1757        if (c == null) {
1758            return contactIds;
1759        }
1760        try {
1761            while (c.moveToNext()) {
1762                contactIds.add(c.getLong(0));
1763            }
1764        } finally {
1765            c.close();
1766        }
1767        return contactIds;
1768    }
1769
1770    public static ContactIconInfo getContactInfo(
1771            final Context context, final String senderAddress,
1772            final int idealIconWidth, final int idealIconHeight,
1773            final int idealWearableBgWidth, final int idealWearableBgHeight) {
1774        final ContactIconInfo contactIconInfo = new ContactIconInfo();
1775        final List<Long> contactIds = findContacts(context, Arrays.asList(
1776                new String[]{senderAddress}));
1777
1778        if (contactIds != null) {
1779            for (final long id : contactIds) {
1780                final Uri contactUri = ContentUris.withAppendedId(
1781                        ContactsContract.Contacts.CONTENT_URI, id);
1782                final InputStream inputStream =
1783                        ContactsContract.Contacts.openContactPhotoInputStream(
1784                                context.getContentResolver(), contactUri, true /*preferHighres*/);
1785
1786                if (inputStream != null) {
1787                    try {
1788                        final Bitmap source = BitmapFactory.decodeStream(inputStream);
1789                        if (source != null) {
1790                            // We should scale this image to fit the intended size
1791                            contactIconInfo.icon = Bitmap.createScaledBitmap(source, idealIconWidth,
1792                                    idealIconHeight, true);
1793
1794                            contactIconInfo.wearableBg = Bitmap.createScaledBitmap(source,
1795                                    idealWearableBgWidth, idealWearableBgHeight, true);
1796                        }
1797
1798                        if (contactIconInfo.icon != null) {
1799                            break;
1800                        }
1801                    } finally {
1802                        Closeables.closeQuietly(inputStream);
1803                    }
1804                }
1805            }
1806        }
1807
1808        return contactIconInfo;
1809    }
1810
1811    private static String getMessageBodyWithoutElidedText(final Message message) {
1812        return getMessageBodyWithoutElidedText(message.getBodyAsHtml());
1813    }
1814
1815    public static String getMessageBodyWithoutElidedText(String html) {
1816        if (TextUtils.isEmpty(html)) {
1817            return "";
1818        }
1819        // Get the html "tree" for this message body
1820        final HtmlTree htmlTree = com.android.mail.utils.Utils.getHtmlTree(html);
1821        htmlTree.setConverterFactory(MESSAGE_CONVERTER_FACTORY);
1822
1823        return htmlTree.getPlainText();
1824    }
1825
1826    public static void markSeen(final Context context, final Folder folder) {
1827        final Uri uri = folder.folderUri.fullUri;
1828
1829        final ContentValues values = new ContentValues(1);
1830        values.put(UIProvider.ConversationColumns.SEEN, 1);
1831
1832        context.getContentResolver().update(uri, values, null, null);
1833    }
1834
1835    /**
1836     * Returns a displayable string representing
1837     * the message sender. It has a preference toward showing the name,
1838     * but will fall back to the address if that is all that is available.
1839     */
1840    private static String getDisplayableSender(String sender) {
1841        final EmailAddress address = EmailAddress.getEmailAddress(sender);
1842
1843        String displayableSender = address.getName();
1844
1845        if (!TextUtils.isEmpty(displayableSender)) {
1846            return Address.decodeAddressPersonal(displayableSender);
1847        }
1848
1849        // If that fails, default to the sender address.
1850        displayableSender = address.getAddress();
1851
1852        // If we were unable to tokenize a name or address,
1853        // just use whatever was in the sender.
1854        if (TextUtils.isEmpty(displayableSender)) {
1855            displayableSender = sender;
1856        }
1857        return displayableSender;
1858    }
1859
1860    /**
1861     * Returns only the address portion of a message sender.
1862     */
1863    private static String getSenderAddress(String sender) {
1864        final EmailAddress address = EmailAddress.getEmailAddress(sender);
1865
1866        String tokenizedAddress = address.getAddress();
1867
1868        // If we were unable to tokenize a name or address,
1869        // just use whatever was in the sender.
1870        if (TextUtils.isEmpty(tokenizedAddress)) {
1871            tokenizedAddress = sender;
1872        }
1873        return tokenizedAddress;
1874    }
1875
1876    /**
1877     * Given a sender, retrieve the email address. If an email address is extracted, add it to the
1878     * input set, otherwise ignore it.
1879     * @param sender
1880     * @param senderAddressesSet
1881     */
1882    private static void addEmailAddressToSet(String sender, HashSet<String> senderAddressesSet) {
1883        // Only continue if we have a non-empty, non-null sender
1884        if (!TextUtils.isEmpty(sender)) {
1885            final EmailAddress address = EmailAddress.getEmailAddress(sender);
1886            final String senderEmailAddress = address.getAddress();
1887
1888            // Add to set only if we have a non-empty email address
1889            if (!TextUtils.isEmpty(senderEmailAddress)) {
1890                senderAddressesSet.add(senderEmailAddress);
1891            } else {
1892                LogUtils.i(LOG_TAG, "Unable to grab email from \"%s\" for notification tagging",
1893                        LogUtils.sanitizeName(LOG_TAG, sender));
1894            }
1895        }
1896    }
1897
1898    public static int getNotificationId(final android.accounts.Account account,
1899            final Folder folder) {
1900        return 1 ^ account.hashCode() ^ folder.hashCode();
1901    }
1902
1903    private static int getNotificationId(int summaryNotificationId, int conversationHashCode) {
1904        return summaryNotificationId ^ conversationHashCode;
1905    }
1906
1907    private static class NotificationKey {
1908        public final Account account;
1909        public final Folder folder;
1910
1911        public NotificationKey(Account account, Folder folder) {
1912            this.account = account;
1913            this.folder = folder;
1914        }
1915
1916        @Override
1917        public boolean equals(Object other) {
1918            if (!(other instanceof NotificationKey)) {
1919                return false;
1920            }
1921            NotificationKey key = (NotificationKey) other;
1922            return account.getAccountManagerAccount().equals(key.account.getAccountManagerAccount())
1923                    && folder.equals(key.folder);
1924        }
1925
1926        @Override
1927        public String toString() {
1928            return account.getDisplayName() + " " + folder.name;
1929        }
1930
1931        @Override
1932        public int hashCode() {
1933            final int accountHashCode = account.getAccountManagerAccount().hashCode();
1934            final int folderHashCode = folder.hashCode();
1935            return accountHashCode ^ folderHashCode;
1936        }
1937    }
1938
1939    /**
1940     * Contains the logic for converting the contents of one HtmlTree into
1941     * plaintext.
1942     */
1943    public static class MailMessagePlainTextConverter extends HtmlTree.DefaultPlainTextConverter {
1944        // Strings for parsing html message bodies
1945        private static final String ELIDED_TEXT_ELEMENT_NAME = "div";
1946        private static final String ELIDED_TEXT_ELEMENT_ATTRIBUTE_NAME = "class";
1947        private static final String ELIDED_TEXT_ELEMENT_ATTRIBUTE_CLASS_VALUE = "elided-text";
1948
1949        private static final HTML.Attribute ELIDED_TEXT_ATTRIBUTE =
1950                new HTML.Attribute(ELIDED_TEXT_ELEMENT_ATTRIBUTE_NAME, HTML.Attribute.NO_TYPE);
1951
1952        private static final HtmlDocument.Node ELIDED_TEXT_REPLACEMENT_NODE =
1953                HtmlDocument.createSelfTerminatingTag(HTML4.BR_ELEMENT, null, null, null);
1954
1955        private int mEndNodeElidedTextBlock = -1;
1956
1957        @Override
1958        public void addNode(HtmlDocument.Node n, int nodeNum, int endNum) {
1959            // If we are in the middle of an elided text block, don't add this node
1960            if (nodeNum < mEndNodeElidedTextBlock) {
1961                return;
1962            } else if (nodeNum == mEndNodeElidedTextBlock) {
1963                super.addNode(ELIDED_TEXT_REPLACEMENT_NODE, nodeNum, endNum);
1964                return;
1965            }
1966
1967            // If this tag starts another elided text block, we want to remember the end
1968            if (n instanceof HtmlDocument.Tag) {
1969                boolean foundElidedTextTag = false;
1970                final HtmlDocument.Tag htmlTag = (HtmlDocument.Tag)n;
1971                final HTML.Element htmlElement = htmlTag.getElement();
1972                if (ELIDED_TEXT_ELEMENT_NAME.equals(htmlElement.getName())) {
1973                    // Make sure that the class is what is expected
1974                    final List<HtmlDocument.TagAttribute> attributes =
1975                            htmlTag.getAttributes(ELIDED_TEXT_ATTRIBUTE);
1976                    for (HtmlDocument.TagAttribute attribute : attributes) {
1977                        if (ELIDED_TEXT_ELEMENT_ATTRIBUTE_CLASS_VALUE.equals(
1978                                attribute.getValue())) {
1979                            // Found an "elided-text" div.  Remember information about this tag
1980                            mEndNodeElidedTextBlock = endNum;
1981                            foundElidedTextTag = true;
1982                            break;
1983                        }
1984                    }
1985                }
1986
1987                if (foundElidedTextTag) {
1988                    return;
1989                }
1990            }
1991
1992            super.addNode(n, nodeNum, endNum);
1993        }
1994    }
1995
1996    /**
1997     * During account setup in Email, we may not have an inbox yet, so the notification setting had
1998     * to be stored in {@link AccountPreferences}. If it is still there, we need to move it to the
1999     * {@link FolderPreferences} now.
2000     */
2001    public static void moveNotificationSetting(final AccountPreferences accountPreferences,
2002            final FolderPreferences folderPreferences) {
2003        if (accountPreferences.isDefaultInboxNotificationsEnabledSet()) {
2004            // If this setting has been changed some other way, don't overwrite it
2005            if (!folderPreferences.isNotificationsEnabledSet()) {
2006                final boolean notificationsEnabled =
2007                        accountPreferences.getDefaultInboxNotificationsEnabled();
2008
2009                folderPreferences.setNotificationsEnabled(notificationsEnabled);
2010            }
2011
2012            accountPreferences.clearDefaultInboxNotificationsEnabled();
2013        }
2014    }
2015
2016    private static class NotificationBuilders {
2017        public final NotificationCompat.Builder notifBuilder;
2018        public final NotificationCompat.WearableExtender wearableNotifBuilder;
2019
2020        private NotificationBuilders(NotificationCompat.Builder notifBuilder,
2021                NotificationCompat.WearableExtender wearableNotifBuilder) {
2022            this.notifBuilder = notifBuilder;
2023            this.wearableNotifBuilder = wearableNotifBuilder;
2024        }
2025
2026        public static NotificationBuilders of(NotificationCompat.Builder notifBuilder,
2027                NotificationCompat.WearableExtender wearableNotifBuilder) {
2028            return new NotificationBuilders(notifBuilder, wearableNotifBuilder);
2029        }
2030    }
2031
2032    private static class ConfigResult {
2033        public String notificationTicker;
2034        public ContactIconInfo contactIconInfo;
2035    }
2036
2037    public static class ContactIconInfo {
2038        public Bitmap icon;
2039        public Bitmap wearableBg;
2040    }
2041}
2042