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