NotificationUtils.java revision 9461561d3f0ea554138c3b24207a69306e07d4bc
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 child notification ids.
115    private static Map<NotificationKey, Set<Integer>> sChildNotificationsMap =
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                cancelChildNotifications(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            cancelChildNotifications(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                cancelChildNotifications(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;
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
615            final long when;
616
617            final long oldWhen =
618                    NotificationActionUtils.sNotificationTimestamps.get(notificationId);
619            if (oldWhen != 0) {
620                when = oldWhen;
621            } else {
622                when = System.currentTimeMillis();
623            }
624
625            notification.setWhen(when);
626
627            // The timestamp is now stored in the notification, so we can remove it from here
628            NotificationActionUtils.sNotificationTimestamps.delete(notificationId);
629
630            // Dispatch a CLEAR_NEW_MAIL_NOTIFICATIONS intent if the user taps the "X" next to a
631            // notification.  Also this intent gets fired when the user taps on a notification as
632            // the AutoCancel flag has been set
633            final Intent cancelNotificationIntent =
634                    new Intent(MailIntentService.ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS);
635            cancelNotificationIntent.setPackage(context.getPackageName());
636            cancelNotificationIntent.setData(Utils.appendVersionQueryParameter(context,
637                    folder.folderUri.fullUri));
638            cancelNotificationIntent.putExtra(Utils.EXTRA_ACCOUNT, account);
639            cancelNotificationIntent.putExtra(Utils.EXTRA_FOLDER, folder);
640
641            notification.setDeleteIntent(PendingIntent.getService(
642                    context, notificationId, cancelNotificationIntent, 0));
643
644            // Ensure that the notification is cleared when the user selects it
645            notification.setAutoCancel(true);
646
647            boolean eventInfoConfigured = false;
648
649            final boolean isInbox = folder.folderUri.equals(account.settings.defaultInbox);
650            final FolderPreferences folderPreferences =
651                    new FolderPreferences(context, account.getEmailAddress(), folder, isInbox);
652
653            if (isInbox) {
654                final AccountPreferences accountPreferences =
655                        new AccountPreferences(context, account.getEmailAddress());
656                moveNotificationSetting(accountPreferences, folderPreferences);
657            }
658
659            if (!folderPreferences.areNotificationsEnabled()) {
660                LogUtils.i(LOG_TAG, "Notifications are disabled for this folder; not notifying");
661                // Don't notify
662                return;
663            }
664
665            if (unreadCount > 0) {
666                // How can I order this properly?
667                if (cursor.moveToNext()) {
668                    final Intent notificationIntent;
669
670                    // Launch directly to the conversation, if there is only 1 unseen conversation
671                    final boolean launchConversationMode = (unseenCount == 1);
672                    if (launchConversationMode) {
673                        notificationIntent = createViewConversationIntent(context, account, folder,
674                                cursor);
675                    } else {
676                        notificationIntent = createViewConversationIntent(context, account, folder,
677                                null);
678                    }
679
680                    Analytics.getInstance().sendEvent("notification_create",
681                            launchConversationMode ? "conversation" : "conversation_list",
682                            folder.getTypeDescription(), unseenCount);
683
684                    if (notificationIntent == null) {
685                        LogUtils.e(LOG_TAG, "Null intent when building notification");
686                        return;
687                    }
688
689                    clickIntent = createClickPendingIntent(context, notificationIntent);
690
691                    configureLatestEventInfoFromConversation(context, account, folderPreferences,
692                            notification, wearableExtender, msgNotifications, notificationId,
693                            cursor, clickIntent, notificationIntent, unreadCount, unseenCount,
694                            folder, when, photoFetcher);
695                    eventInfoConfigured = true;
696                }
697            }
698
699            final boolean vibrate = folderPreferences.isNotificationVibrateEnabled();
700            final String ringtoneUri = folderPreferences.getNotificationRingtoneUri();
701            final boolean notifyOnce = !folderPreferences.isEveryMessageNotificationEnabled();
702
703            if (!ignoreUnobtrusiveSetting && notifyOnce) {
704                // If the user has "unobtrusive notifications" enabled, only alert the first time
705                // new mail is received in this account.  This is the default behavior.  See
706                // bugs 2412348 and 2413490.
707                LogUtils.d(LOG_TAG, "Setting Alert Once");
708                notification.setOnlyAlertOnce(true);
709            }
710
711            LogUtils.i(LOG_TAG, "Account: %s vibrate: %s",
712                    LogUtils.sanitizeName(LOG_TAG, account.getEmailAddress()),
713                    Boolean.toString(folderPreferences.isNotificationVibrateEnabled()));
714
715            int defaults = 0;
716
717            // Check if any current child notifications exist previously.  Only notify if one of
718            // them is new.
719            boolean hasNewChildNotification;
720            Set<Integer> prevChildNotifications = sChildNotificationsMap.get(notificationKey);
721            if (prevChildNotifications != null) {
722                hasNewChildNotification = false;
723                for (Integer currentNotificationId : msgNotifications.keySet()) {
724                    if (!prevChildNotifications.contains(currentNotificationId)) {
725                        hasNewChildNotification = true;
726                        break;
727                    }
728                }
729            } else {
730                hasNewChildNotification = true;
731            }
732
733            LogUtils.d(LOG_TAG, "getAttention=%s,oldWhen=%s,hasNewChildNotification=%s",
734                    getAttention, oldWhen, hasNewChildNotification);
735
736            /*
737             * We do not want to notify if this is coming back from an Undo notification, hence the
738             * oldWhen check.
739             */
740            if (getAttention && oldWhen == 0 && hasNewChildNotification) {
741                final AccountPreferences accountPreferences =
742                        new AccountPreferences(context, account.getEmailAddress());
743                if (accountPreferences.areNotificationsEnabled()) {
744                    if (vibrate) {
745                        defaults |= Notification.DEFAULT_VIBRATE;
746                    }
747
748                    notification.setSound(TextUtils.isEmpty(ringtoneUri) ? null
749                            : Uri.parse(ringtoneUri));
750                    LogUtils.i(LOG_TAG, "New email in %s vibrateWhen: %s, playing notification: %s",
751                            LogUtils.sanitizeName(LOG_TAG, account.getEmailAddress()), vibrate,
752                            ringtoneUri);
753                }
754            }
755
756            // TODO(skennedy) Why do we do any of the above if we're just going to bail here?
757            if (eventInfoConfigured) {
758                defaults |= Notification.DEFAULT_LIGHTS;
759                notification.setDefaults(defaults);
760
761                if (oldWhen != 0) {
762                    // We do not want to display the ticker again if we are re-displaying this
763                    // notification (like from an Undo notification)
764                    notification.setTicker(null);
765                }
766
767                notification.extend(wearableExtender);
768                nm.notify(notificationId, notification.build());
769
770                if (prevChildNotifications != null) {
771                    Set<Integer> currentNotificationIds = msgNotifications.keySet();
772                    for (Integer prevChildNotificationId : prevChildNotifications) {
773                        if (!currentNotificationIds.contains(prevChildNotificationId)) {
774                            nm.cancel(prevChildNotificationId);
775                            LogUtils.d(LOG_TAG, "canceling child notification %s",
776                                    prevChildNotificationId);
777                        }
778                    }
779                }
780
781                for (Map.Entry<Integer, NotificationBuilders> entry : msgNotifications.entrySet()) {
782                    NotificationBuilders builders = entry.getValue();
783                    builders.notifBuilder.extend(builders.wearableNotifBuilder);
784                    nm.notify(entry.getKey(), builders.notifBuilder.build());
785                    LogUtils.d(LOG_TAG, "notifying child notification %s", entry.getKey());
786                }
787
788                Set<Integer> childNotificationIds = new HashSet<Integer>();
789                childNotificationIds.addAll(msgNotifications.keySet());
790                sChildNotificationsMap.put(notificationKey, childNotificationIds);
791            } else {
792                LogUtils.i(LOG_TAG, "event info not configured - not notifying");
793            }
794        } finally {
795            if (cursor != null) {
796                cursor.close();
797            }
798        }
799    }
800
801    private static PendingIntent createClickPendingIntent(Context context,
802            Intent notificationIntent) {
803        // Amend the click intent with a hint that its source was a notification,
804        // but remove the hint before it's used to generate notification action
805        // intents. This prevents the following sequence:
806        // 1. generate single notification
807        // 2. user clicks reply, then completes Compose activity
808        // 3. main activity launches, gets FROM_NOTIFICATION hint in intent
809        notificationIntent.putExtra(Utils.EXTRA_FROM_NOTIFICATION, true);
810        PendingIntent clickIntent = PendingIntent.getActivity(context, -1, notificationIntent,
811                PendingIntent.FLAG_UPDATE_CURRENT);
812        notificationIntent.removeExtra(Utils.EXTRA_FROM_NOTIFICATION);
813        return clickIntent;
814    }
815
816    /**
817     * @return an {@link Intent} which, if launched, will display the corresponding conversation
818     */
819    private static Intent createViewConversationIntent(final Context context, final Account account,
820            final Folder folder, final Cursor cursor) {
821        if (folder == null || account == null) {
822            LogUtils.e(LOG_TAG, "createViewConversationIntent(): "
823                    + "Null account or folder.  account: %s folder: %s", account, folder);
824            return null;
825        }
826
827        final Intent intent;
828
829        if (cursor == null) {
830            intent = Utils.createViewFolderIntent(context, folder.folderUri.fullUri, account);
831        } else {
832            // A conversation cursor has been specified, so this intent is intended to be go
833            // directly to the one new conversation
834
835            // Get the Conversation object
836            final Conversation conversation = new Conversation(cursor);
837            intent = Utils.createViewConversationIntent(context, conversation,
838                    folder.folderUri.fullUri, account);
839        }
840
841        return intent;
842    }
843
844    private static Bitmap getDefaultNotificationIcon(
845            final Context context, final Folder folder, final boolean multipleNew) {
846        final int resId;
847        if (folder.notificationIconResId != 0) {
848            resId = folder.notificationIconResId;
849        } else if (multipleNew) {
850            resId = R.drawable.ic_notification_multiple_mail_holo_dark;
851        } else {
852            resId = R.drawable.ic_contact_picture;
853        }
854
855        final Bitmap icon = getIcon(context, resId);
856
857        if (icon == null) {
858            LogUtils.e(LOG_TAG, "Couldn't decode notif icon res id %d", resId);
859        }
860
861        return icon;
862    }
863
864    private static Bitmap getIcon(final Context context, final int resId) {
865        final Bitmap cachedIcon = sNotificationIcons.get(resId);
866        if (cachedIcon != null) {
867            return cachedIcon;
868        }
869
870        final Bitmap icon = BitmapFactory.decodeResource(context.getResources(), resId);
871        sNotificationIcons.put(resId, icon);
872
873        return icon;
874    }
875
876    private static Bitmap getDefaultWearableBg(Context context) {
877        Bitmap bg = sDefaultWearableBg.get();
878        if (bg == null) {
879            bg = BitmapFactory.decodeResource(context.getResources(), R.drawable.bg_email);
880            sDefaultWearableBg = new WeakReference<Bitmap>(bg);
881        }
882        return bg;
883    }
884
885    private static void configureLatestEventInfoFromConversation(final Context context,
886            final Account account, final FolderPreferences folderPreferences,
887            final NotificationCompat.Builder notification,
888            final NotificationCompat.WearableExtender wearableExtender,
889            final Map<Integer, NotificationBuilders> msgNotifications,
890            final int summaryNotificationId, final Cursor conversationCursor,
891            final PendingIntent clickIntent, final Intent notificationIntent,
892            final int unreadCount, final int unseenCount,
893            final Folder folder, final long when, final ContactPhotoFetcher photoFetcher) {
894        final Resources res = context.getResources();
895        final String notificationAccountDisplayName = account.getDisplayName();
896        final String notificationAccountEmail = account.getEmailAddress();
897
898        LogUtils.i(LOG_TAG, "Showing notification with unreadCount of %d and unseenCount of %d",
899                unreadCount, unseenCount);
900
901        String notificationTicker = null;
902
903        // Boolean indicating that this notification is for a non-inbox label.
904        final boolean isInbox = folder.folderUri.fullUri.equals(account.settings.defaultInbox);
905
906        // Notification label name for user label notifications.
907        final String notificationLabelName = isInbox ? null : folder.name;
908
909        if (unseenCount > 1) {
910            // Build the string that describes the number of new messages
911            final String newMessagesString = res.getString(R.string.new_messages, unseenCount);
912
913            // Use the default notification icon
914            notification.setLargeIcon(
915                    getDefaultNotificationIcon(context, folder, true /* multiple new messages */));
916
917            // The ticker initially start as the new messages string.
918            notificationTicker = newMessagesString;
919
920            // The title of the notification is the new messages string
921            notification.setContentTitle(newMessagesString);
922
923            // TODO(skennedy) Can we remove this check?
924            if (com.android.mail.utils.Utils.isRunningJellybeanOrLater()) {
925                // For a new-style notification
926                final int maxNumDigestItems = context.getResources().getInteger(
927                        R.integer.max_num_notification_digest_items);
928
929                // The body of the notification is the account name, or the label name.
930                notification.setSubText(
931                        isInbox ? notificationAccountDisplayName : notificationLabelName);
932
933                final NotificationCompat.InboxStyle digest =
934                        new NotificationCompat.InboxStyle(notification);
935
936                // Group by account.
937                String notificationGroupKey =
938                        account.uri.toString() + "/" + folder.folderUri.fullUri;
939                notification.setGroup(notificationGroupKey).setGroupSummary(true);
940
941                ConfigResult firstResult = null;
942                int numDigestItems = 0;
943                do {
944                    final Conversation conversation = new Conversation(conversationCursor);
945
946                    if (!conversation.read) {
947                        boolean multipleUnreadThread = false;
948                        // TODO(cwren) extract this pattern into a helper
949
950                        Cursor cursor = null;
951                        MessageCursor messageCursor = null;
952                        try {
953                            final Uri.Builder uriBuilder = conversation.messageListUri.buildUpon();
954                            uriBuilder.appendQueryParameter(
955                                    UIProvider.LABEL_QUERY_PARAMETER, notificationLabelName);
956                            cursor = context.getContentResolver().query(uriBuilder.build(),
957                                    UIProvider.MESSAGE_PROJECTION, null, null, null);
958                            messageCursor = new MessageCursor(cursor);
959
960                            String from = "";
961                            String fromAddress = "";
962                            if (messageCursor.moveToPosition(messageCursor.getCount() - 1)) {
963                                final Message message = messageCursor.getMessage();
964                                fromAddress = message.getFrom();
965                                if (fromAddress == null) {
966                                    fromAddress = "";
967                                }
968                                from = getDisplayableSender(fromAddress);
969                            }
970                            while (messageCursor.moveToPosition(messageCursor.getPosition() - 1)) {
971                                final Message message = messageCursor.getMessage();
972                                if (!message.read &&
973                                        !fromAddress.contentEquals(message.getFrom())) {
974                                    multipleUnreadThread = true;
975                                    break;
976                                }
977                            }
978                            final SpannableStringBuilder sendersBuilder;
979                            if (multipleUnreadThread) {
980                                final int sendersLength =
981                                        res.getInteger(R.integer.swipe_senders_length);
982
983                                sendersBuilder = getStyledSenders(context, conversationCursor,
984                                        sendersLength, notificationAccountEmail);
985                            } else {
986                                sendersBuilder =
987                                        new SpannableStringBuilder(getWrappedFromString(from));
988                            }
989                            final CharSequence digestLine = getSingleMessageInboxLine(context,
990                                    sendersBuilder.toString(),
991                                    ConversationItemView.filterTag(context, conversation.subject),
992                                    conversation.getSnippet());
993                            digest.addLine(digestLine);
994                            numDigestItems++;
995
996                            // Adding child notification for Wear.
997                            NotificationCompat.Builder childNotif =
998                                    new NotificationCompat.Builder(context);
999                            childNotif.setSmallIcon(R.drawable.stat_notify_email);
1000                            childNotif.setContentText(digestLine);
1001                            Intent childNotificationIntent = createViewConversationIntent(context,
1002                                    account, folder, conversationCursor);
1003                            PendingIntent childClickIntent = createClickPendingIntent(context,
1004                                    childNotificationIntent);
1005                            childNotif.setContentIntent(childClickIntent);
1006                            childNotif.setAutoCancel(true);
1007
1008                            // TODO: Use a stable sort key if possible, e.g. message post time
1009                            // + msgid hash
1010                            String groupSortKey = String.format("%010d", numDigestItems);
1011                            childNotif.setGroup(notificationGroupKey);
1012                            childNotif.setSortKey(groupSortKey);
1013
1014                            int childNotificationId = getNotificationId(summaryNotificationId,
1015                                    conversation.hashCode());
1016
1017                            final NotificationCompat.WearableExtender childWearExtender =
1018                                    new NotificationCompat.WearableExtender();
1019                            final ConfigResult result =
1020                                    configureNotifForOneConversation(context, account,
1021                                    folderPreferences, childNotif, childWearExtender,
1022                                    conversationCursor, notificationIntent, folder, when, res,
1023                                    notificationAccountDisplayName, notificationAccountEmail,
1024                                    isInbox, notificationLabelName, childNotificationId,
1025                                    photoFetcher);
1026                            msgNotifications.put(childNotificationId,
1027                                    NotificationBuilders.of(childNotif, childWearExtender));
1028
1029                            if (firstResult == null) {
1030                                firstResult = result;
1031                            }
1032                        } finally {
1033                            if (messageCursor != null) {
1034                                messageCursor.close();
1035                            }
1036                            if (cursor != null) {
1037                                cursor.close();
1038                            }
1039                        }
1040                    }
1041                } while (numDigestItems <= maxNumDigestItems && conversationCursor.moveToNext());
1042
1043                if (firstResult != null && firstResult.contactIconInfo != null) {
1044                    wearableExtender.setBackground(firstResult.contactIconInfo.wearableBg);
1045                } else {
1046                    LogUtils.w(LOG_TAG, "First contact icon is null!");
1047                    wearableExtender.setBackground(getDefaultWearableBg(context));
1048                }
1049            } else {
1050                // The body of the notification is the account name, or the label name.
1051                notification.setContentText(
1052                        isInbox ? notificationAccountDisplayName : notificationLabelName);
1053            }
1054        } else {
1055            // For notifications for a single new conversation, we want to get the information
1056            // from the conversation
1057
1058            // Move the cursor to the most recent unread conversation
1059            seekToLatestUnreadConversation(conversationCursor);
1060
1061            final ConfigResult result = configureNotifForOneConversation(context, account,
1062                    folderPreferences, notification, wearableExtender, conversationCursor,
1063                    notificationIntent, folder, when, res, notificationAccountDisplayName,
1064                    notificationAccountEmail, isInbox, notificationLabelName,
1065                    summaryNotificationId, photoFetcher);
1066            notificationTicker = result.notificationTicker;
1067
1068            wearableExtender.setBackground(result.contactIconInfo.wearableBg);
1069        }
1070
1071        // Build the notification ticker
1072        if (notificationLabelName != null && notificationTicker != null) {
1073            // This is a per label notification, format the ticker with that information
1074            notificationTicker = res.getString(R.string.label_notification_ticker,
1075                    notificationLabelName, notificationTicker);
1076        }
1077
1078        if (notificationTicker != null) {
1079            // If we didn't generate a notification ticker, it will default to account name
1080            notification.setTicker(notificationTicker);
1081        }
1082
1083        // Set the number in the notification
1084        if (unreadCount > 1) {
1085            notification.setNumber(unreadCount);
1086        }
1087
1088        notification.setContentIntent(clickIntent);
1089    }
1090
1091    /**
1092     * Configure the notification for one conversation.  When there are multiple conversations,
1093     * this method is used to configure bundled notification for Android Wear.
1094     */
1095    private static ConfigResult configureNotifForOneConversation(Context context,
1096            Account account, FolderPreferences folderPreferences,
1097            NotificationCompat.Builder notification,
1098            NotificationCompat.WearableExtender wearExtender, Cursor conversationCursor,
1099            Intent notificationIntent, Folder folder, long when, Resources res,
1100            String notificationAccountDisplayName, String notificationAccountEmail,
1101            boolean isInbox, String notificationLabelName, int notificationId,
1102            final ContactPhotoFetcher photoFetcher) {
1103
1104        final ConfigResult result = new ConfigResult();
1105
1106        final Conversation conversation = new Conversation(conversationCursor);
1107
1108        Cursor cursor = null;
1109        MessageCursor messageCursor = null;
1110        boolean multipleUnseenThread = false;
1111        String from = null;
1112        try {
1113            final Uri uri = conversation.messageListUri.buildUpon().appendQueryParameter(
1114                    UIProvider.LABEL_QUERY_PARAMETER, folder.persistentId).build();
1115            cursor = context.getContentResolver().query(uri, UIProvider.MESSAGE_PROJECTION,
1116                    null, null, null);
1117            messageCursor = new MessageCursor(cursor);
1118            // Use the information from the last sender in the conversation that triggered
1119            // this notification.
1120
1121            String fromAddress = "";
1122            if (messageCursor.moveToPosition(messageCursor.getCount() - 1)) {
1123                final Message message = messageCursor.getMessage();
1124                fromAddress = message.getFrom();
1125                if (fromAddress == null) {
1126                    // No sender. Go back to default value.
1127                    LogUtils.e(LOG_TAG, "No sender found for message: %d" + message.getId());
1128                    fromAddress = "";
1129                }
1130                from = getDisplayableSender(fromAddress);
1131                result.contactIconInfo = getContactIcon(
1132                        context, account.getAccountManagerAccount().name, from,
1133                        getSenderAddress(fromAddress), folder, photoFetcher);
1134                notification.setLargeIcon(result.contactIconInfo.icon);
1135            }
1136
1137            // Assume that the last message in this conversation is unread
1138            int firstUnseenMessagePos = messageCursor.getPosition();
1139            while (messageCursor.moveToPosition(messageCursor.getPosition() - 1)) {
1140                final Message message = messageCursor.getMessage();
1141                final boolean unseen = !message.seen;
1142                if (unseen) {
1143                    firstUnseenMessagePos = messageCursor.getPosition();
1144                    if (!multipleUnseenThread
1145                            && !fromAddress.contentEquals(message.getFrom())) {
1146                        multipleUnseenThread = true;
1147                    }
1148                }
1149            }
1150
1151            final String subject = ConversationItemView.filterTag(context, conversation.subject);
1152
1153            // TODO(skennedy) Can we remove this check?
1154            if (Utils.isRunningJellybeanOrLater()) {
1155                // For a new-style notification
1156
1157                if (multipleUnseenThread) {
1158                    // The title of a single conversation is the list of senders.
1159                    int sendersLength = res.getInteger(R.integer.swipe_senders_length);
1160
1161                    final SpannableStringBuilder sendersBuilder = getStyledSenders(
1162                            context, conversationCursor, sendersLength,
1163                            notificationAccountEmail);
1164
1165                    notification.setContentTitle(sendersBuilder);
1166                    // For a single new conversation, the ticker is based on the sender's name.
1167                    result.notificationTicker = sendersBuilder.toString();
1168                } else {
1169                    from = getWrappedFromString(from);
1170                    // The title of a single message the sender.
1171                    notification.setContentTitle(from);
1172                    // For a single new conversation, the ticker is based on the sender's name.
1173                    result.notificationTicker = from;
1174                }
1175
1176                // The notification content will be the subject of the conversation.
1177                notification.setContentText(getSingleMessageLittleText(context, subject));
1178
1179                // The notification subtext will be the subject of the conversation for inbox
1180                // notifications, or will based on the the label name for user label
1181                // notifications.
1182                notification.setSubText(isInbox ?
1183                        notificationAccountDisplayName : notificationLabelName);
1184
1185                if (multipleUnseenThread) {
1186                    notification.setLargeIcon(
1187                            getDefaultNotificationIcon(context, folder, true));
1188                }
1189                final NotificationCompat.BigTextStyle bigText =
1190                        new NotificationCompat.BigTextStyle(notification);
1191
1192                // Seek the message cursor to the first unread message
1193                final Message message;
1194                if (messageCursor.moveToPosition(firstUnseenMessagePos)) {
1195                    message = messageCursor.getMessage();
1196                    bigText.bigText(getSingleMessageBigText(context, subject, message));
1197                } else {
1198                    LogUtils.e(LOG_TAG, "Failed to load message");
1199                    message = null;
1200                }
1201
1202                if (message != null) {
1203                    final Set<String> notificationActions =
1204                            folderPreferences.getNotificationActions(account);
1205
1206                    NotificationActionUtils.addNotificationActions(context, notificationIntent,
1207                            notification, wearExtender, account, conversation, message,
1208                            folder, notificationId, when, notificationActions);
1209                }
1210            } else {
1211                // For an old-style notification
1212
1213                // The title of a single conversation notification is built from both the sender
1214                // and subject of the new message.
1215                notification.setContentTitle(
1216                        getSingleMessageNotificationTitle(context, from, subject));
1217
1218                // The notification content will be the subject of the conversation for inbox
1219                // notifications, or will based on the the label name for user label
1220                // notifications.
1221                notification.setContentText(
1222                        isInbox ? notificationAccountDisplayName : notificationLabelName);
1223
1224                // For a single new conversation, the ticker is based on the sender's name.
1225                result.notificationTicker = from;
1226            }
1227        } finally {
1228            if (messageCursor != null) {
1229                messageCursor.close();
1230            }
1231            if (cursor != null) {
1232                cursor.close();
1233            }
1234        }
1235        return result;
1236    }
1237
1238    private static String getWrappedFromString(String from) {
1239        if (from == null) {
1240            LogUtils.e(LOG_TAG, "null from string in getWrappedFromString");
1241            from = "";
1242        }
1243        from = sBidiFormatter.unicodeWrap(from);
1244        return from;
1245    }
1246
1247    private static SpannableStringBuilder getStyledSenders(final Context context,
1248            final Cursor conversationCursor, final int maxLength, final String account) {
1249        final Conversation conversation = new Conversation(conversationCursor);
1250        final com.android.mail.providers.ConversationInfo conversationInfo =
1251                conversation.conversationInfo;
1252        final ArrayList<SpannableString> senders = new ArrayList<SpannableString>();
1253        if (sNotificationUnreadStyleSpan == null) {
1254            sNotificationUnreadStyleSpan = new TextAppearanceSpan(
1255                    context, R.style.NotificationSendersUnreadTextAppearance);
1256            sNotificationReadStyleSpan =
1257                    new TextAppearanceSpan(context, R.style.NotificationSendersReadTextAppearance);
1258        }
1259        SendersView.format(context, conversationInfo, "", maxLength, senders, null, null, account,
1260                sNotificationUnreadStyleSpan, sNotificationReadStyleSpan,
1261                false /* showToHeader */, false /* resourceCachingRequired */);
1262
1263        return ellipsizeStyledSenders(context, senders);
1264    }
1265
1266    private static String sSendersSplitToken = null;
1267    private static String sElidedPaddingToken = null;
1268
1269    private static SpannableStringBuilder ellipsizeStyledSenders(final Context context,
1270            ArrayList<SpannableString> styledSenders) {
1271        if (sSendersSplitToken == null) {
1272            sSendersSplitToken = context.getString(R.string.senders_split_token);
1273            sElidedPaddingToken = context.getString(R.string.elided_padding_token);
1274        }
1275
1276        SpannableStringBuilder builder = new SpannableStringBuilder();
1277        SpannableString prevSender = null;
1278        for (SpannableString sender : styledSenders) {
1279            if (sender == null) {
1280                LogUtils.e(LOG_TAG, "null sender iterating over styledSenders");
1281                continue;
1282            }
1283            CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class);
1284            if (SendersView.sElidedString.equals(sender.toString())) {
1285                prevSender = sender;
1286                sender = copyStyles(spans, sElidedPaddingToken + sender + sElidedPaddingToken);
1287            } else if (builder.length() > 0
1288                    && (prevSender == null || !SendersView.sElidedString.equals(prevSender
1289                            .toString()))) {
1290                prevSender = sender;
1291                sender = copyStyles(spans, sSendersSplitToken + sender);
1292            } else {
1293                prevSender = sender;
1294            }
1295            builder.append(sender);
1296        }
1297        return builder;
1298    }
1299
1300    private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) {
1301        SpannableString s = new SpannableString(newText);
1302        if (spans != null && spans.length > 0) {
1303            s.setSpan(spans[0], 0, s.length(), 0);
1304        }
1305        return s;
1306    }
1307
1308    /**
1309     * Seeks the cursor to the position of the most recent unread conversation. If no unread
1310     * conversation is found, the position of the cursor will be restored, and false will be
1311     * returned.
1312     */
1313    private static boolean seekToLatestUnreadConversation(final Cursor cursor) {
1314        final int initialPosition = cursor.getPosition();
1315        do {
1316            final Conversation conversation = new Conversation(cursor);
1317            if (!conversation.read) {
1318                return true;
1319            }
1320        } while (cursor.moveToNext());
1321
1322        // Didn't find an unread conversation, reset the position.
1323        cursor.moveToPosition(initialPosition);
1324        return false;
1325    }
1326
1327    /**
1328     * Sets the bigtext for a notification for a single new conversation
1329     *
1330     * @param context
1331     * @param senders Sender of the new message that triggered the notification.
1332     * @param subject Subject of the new message that triggered the notification
1333     * @param snippet Snippet of the new message that triggered the notification
1334     * @return a {@link CharSequence} suitable for use in
1335     *         {@link android.support.v4.app.NotificationCompat.BigTextStyle}
1336     */
1337    private static CharSequence getSingleMessageInboxLine(Context context,
1338            String senders, String subject, String snippet) {
1339        // TODO(cwren) finish this step toward commmon code with getSingleMessageBigText
1340
1341        final String subjectSnippet = !TextUtils.isEmpty(subject) ? subject : snippet;
1342
1343        final TextAppearanceSpan notificationPrimarySpan =
1344                new TextAppearanceSpan(context, R.style.NotificationPrimaryText);
1345
1346        if (TextUtils.isEmpty(senders)) {
1347            // If the senders are empty, just use the subject/snippet.
1348            return subjectSnippet;
1349        } else if (TextUtils.isEmpty(subjectSnippet)) {
1350            // If the subject/snippet is empty, just use the senders.
1351            final SpannableString spannableString = new SpannableString(senders);
1352            spannableString.setSpan(notificationPrimarySpan, 0, senders.length(), 0);
1353
1354            return spannableString;
1355        } else {
1356            final String formatString = context.getResources().getString(
1357                    R.string.multiple_new_message_notification_item);
1358            final TextAppearanceSpan notificationSecondarySpan =
1359                    new TextAppearanceSpan(context, R.style.NotificationSecondaryText);
1360
1361            // senders is already individually unicode wrapped so it does not need to be done here
1362            final String instantiatedString = String.format(formatString,
1363                    senders,
1364                    sBidiFormatter.unicodeWrap(subjectSnippet));
1365
1366            final SpannableString spannableString = new SpannableString(instantiatedString);
1367
1368            final boolean isOrderReversed = formatString.indexOf("%2$s") <
1369                    formatString.indexOf("%1$s");
1370            final int primaryOffset =
1371                    (isOrderReversed ? instantiatedString.lastIndexOf(senders) :
1372                     instantiatedString.indexOf(senders));
1373            final int secondaryOffset =
1374                    (isOrderReversed ? instantiatedString.lastIndexOf(subjectSnippet) :
1375                     instantiatedString.indexOf(subjectSnippet));
1376            spannableString.setSpan(notificationPrimarySpan,
1377                    primaryOffset, primaryOffset + senders.length(), 0);
1378            spannableString.setSpan(notificationSecondarySpan,
1379                    secondaryOffset, secondaryOffset + subjectSnippet.length(), 0);
1380            return spannableString;
1381        }
1382    }
1383
1384    /**
1385     * Sets the bigtext for a notification for a single new conversation
1386     * @param context
1387     * @param subject Subject of the new message that triggered the notification
1388     * @return a {@link CharSequence} suitable for use in
1389     * {@link NotificationCompat.Builder#setContentText}
1390     */
1391    private static CharSequence getSingleMessageLittleText(Context context, String subject) {
1392        final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
1393                context, R.style.NotificationPrimaryText);
1394
1395        final SpannableString spannableString = new SpannableString(subject);
1396        spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0);
1397
1398        return spannableString;
1399    }
1400
1401    /**
1402     * Sets the bigtext for a notification for a single new conversation
1403     *
1404     * @param context
1405     * @param subject Subject of the new message that triggered the notification
1406     * @param message the {@link Message} to be displayed.
1407     * @return a {@link CharSequence} suitable for use in
1408     *         {@link android.support.v4.app.NotificationCompat.BigTextStyle}
1409     */
1410    private static CharSequence getSingleMessageBigText(Context context, String subject,
1411            final Message message) {
1412
1413        final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
1414                context, R.style.NotificationPrimaryText);
1415
1416        final String snippet = getMessageBodyWithoutElidedText(message);
1417
1418        // Change multiple newlines (with potential white space between), into a single new line
1419        final String collapsedSnippet =
1420                !TextUtils.isEmpty(snippet) ? snippet.replaceAll("\\n\\s+", "\n") : "";
1421
1422        if (TextUtils.isEmpty(subject)) {
1423            // If the subject is empty, just use the snippet.
1424            return snippet;
1425        } else if (TextUtils.isEmpty(collapsedSnippet)) {
1426            // If the snippet is empty, just use the subject.
1427            final SpannableString spannableString = new SpannableString(subject);
1428            spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0);
1429
1430            return spannableString;
1431        } else {
1432            final String notificationBigTextFormat = context.getResources().getString(
1433                    R.string.single_new_message_notification_big_text);
1434
1435            // Localizers may change the order of the parameters, look at how the format
1436            // string is structured.
1437            final boolean isSubjectFirst = notificationBigTextFormat.indexOf("%2$s") >
1438                    notificationBigTextFormat.indexOf("%1$s");
1439            final String bigText =
1440                    String.format(notificationBigTextFormat, subject, collapsedSnippet);
1441            final SpannableString spannableString = new SpannableString(bigText);
1442
1443            final int subjectOffset =
1444                    (isSubjectFirst ? bigText.indexOf(subject) : bigText.lastIndexOf(subject));
1445            spannableString.setSpan(notificationSubjectSpan,
1446                    subjectOffset, subjectOffset + subject.length(), 0);
1447
1448            return spannableString;
1449        }
1450    }
1451
1452    /**
1453     * Gets the title for a notification for a single new conversation
1454     * @param context
1455     * @param sender Sender of the new message that triggered the notification.
1456     * @param subject Subject of the new message that triggered the notification
1457     * @return a {@link CharSequence} suitable for use as a {@link Notification} title.
1458     */
1459    private static CharSequence getSingleMessageNotificationTitle(Context context,
1460            String sender, String subject) {
1461
1462        if (TextUtils.isEmpty(subject)) {
1463            // If the subject is empty, just set the title to the sender's information.
1464            return sender;
1465        } else {
1466            final String notificationTitleFormat = context.getResources().getString(
1467                    R.string.single_new_message_notification_title);
1468
1469            // Localizers may change the order of the parameters, look at how the format
1470            // string is structured.
1471            final boolean isSubjectLast = notificationTitleFormat.indexOf("%2$s") >
1472                    notificationTitleFormat.indexOf("%1$s");
1473            final String titleString = String.format(notificationTitleFormat, sender, subject);
1474
1475            // Format the string so the subject is using the secondaryText style
1476            final SpannableString titleSpannable = new SpannableString(titleString);
1477
1478            // Find the offset of the subject.
1479            final int subjectOffset =
1480                    isSubjectLast ? titleString.lastIndexOf(subject) : titleString.indexOf(subject);
1481            final TextAppearanceSpan notificationSubjectSpan =
1482                    new TextAppearanceSpan(context, R.style.NotificationSecondaryText);
1483            titleSpannable.setSpan(notificationSubjectSpan,
1484                    subjectOffset, subjectOffset + subject.length(), 0);
1485            return titleSpannable;
1486        }
1487    }
1488
1489    /**
1490     * Clears the notifications for the specified account/folder.
1491     */
1492    public static void clearFolderNotification(Context context, Account account, Folder folder,
1493            final boolean markSeen) {
1494        LogUtils.v(LOG_TAG, "Clearing all notifications for %s/%s", account.getEmailAddress(),
1495                folder.name);
1496        final NotificationMap notificationMap = getNotificationMap(context);
1497        final NotificationKey key = new NotificationKey(account, folder);
1498        notificationMap.remove(key);
1499        notificationMap.saveNotificationMap(context);
1500
1501        final NotificationManagerCompat notificationManager =
1502                NotificationManagerCompat.from(context);
1503        notificationManager.cancel(getNotificationId(account.getAccountManagerAccount(), folder));
1504
1505        cancelChildNotifications(key, notificationManager);
1506
1507        if (markSeen) {
1508            markSeen(context, folder);
1509        }
1510    }
1511
1512    /**
1513     * Clears all notifications for the specified account.
1514     */
1515    public static void clearAccountNotifications(final Context context,
1516            final android.accounts.Account account) {
1517        LogUtils.v(LOG_TAG, "Clearing all notifications for %s", account);
1518        final NotificationMap notificationMap = getNotificationMap(context);
1519
1520        // Find all NotificationKeys for this account
1521        final ImmutableList.Builder<NotificationKey> keyBuilder = ImmutableList.builder();
1522
1523        for (final NotificationKey key : notificationMap.keySet()) {
1524            if (account.equals(key.account.getAccountManagerAccount())) {
1525                keyBuilder.add(key);
1526            }
1527        }
1528
1529        final List<NotificationKey> notificationKeys = keyBuilder.build();
1530
1531        final NotificationManagerCompat notificationManager =
1532                NotificationManagerCompat.from(context);
1533
1534        for (final NotificationKey notificationKey : notificationKeys) {
1535            final Folder folder = notificationKey.folder;
1536            notificationManager.cancel(getNotificationId(account, folder));
1537            notificationMap.remove(notificationKey);
1538
1539            cancelChildNotifications(notificationKey, notificationManager);
1540        }
1541
1542        notificationMap.saveNotificationMap(context);
1543    }
1544
1545    private static void cancelChildNotifications(NotificationKey key,
1546            NotificationManagerCompat nm) {
1547        Set<Integer> childNotifications = sChildNotificationsMap.get(key);
1548        if (childNotifications != null) {
1549            for (Integer childNotification : childNotifications) {
1550                nm.cancel(childNotification);
1551            }
1552            sChildNotificationsMap.remove(key);
1553        }
1554    }
1555
1556    private static ContactIconInfo getContactIcon(final Context context, String accountName,
1557            final String displayName, final String senderAddress, final Folder folder,
1558            final ContactPhotoFetcher photoFetcher) {
1559
1560        if (senderAddress == null) {
1561            return null;
1562        }
1563
1564        if (Looper.myLooper() == Looper.getMainLooper()) {
1565            throw new IllegalStateException(
1566                    "getContactIcon should not be called on the main thread.");
1567        }
1568
1569        // Get the ideal size for this icon.
1570        final Resources res = context.getResources();
1571        final int idealIconHeight =
1572                res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
1573        final int idealIconWidth =
1574                res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
1575        final int idealWearableBgWidth =
1576                res.getDimensionPixelSize(R.dimen.wearable_background_width);
1577        final int idealWearableBgHeight =
1578                res.getDimensionPixelSize(R.dimen.wearable_background_height);
1579
1580        final ContactIconInfo contactIconInfo =
1581                photoFetcher != null ? photoFetcher.getContactPhoto(
1582                context, accountName, senderAddress,idealIconWidth, idealIconHeight,
1583                idealWearableBgWidth, idealWearableBgHeight) :
1584                getContactInfo(context, senderAddress, idealIconWidth, idealIconHeight,
1585                idealWearableBgWidth, idealWearableBgHeight);
1586
1587        if (contactIconInfo.icon == null) {
1588            // Make a colorful tile!
1589            final Dimensions dimensions = new Dimensions(idealIconWidth, idealIconHeight,
1590                    Dimensions.SCALE_ONE);
1591
1592            contactIconInfo.icon = new LetterTileProvider(context).getLetterTile(dimensions,
1593                    displayName, senderAddress);
1594        }
1595
1596        if (contactIconInfo.icon == null) {
1597            // Icon should be the default mail icon.
1598            contactIconInfo.icon = getDefaultNotificationIcon(context, folder,
1599                    false /* single new message */);
1600        }
1601
1602        if (contactIconInfo.wearableBg == null) {
1603            contactIconInfo.wearableBg = getDefaultWearableBg(context);
1604        }
1605
1606        return contactIconInfo;
1607    }
1608
1609    private static ArrayList<Long> findContacts(Context context, Collection<String> addresses) {
1610        ArrayList<String> whereArgs = new ArrayList<String>();
1611        StringBuilder whereBuilder = new StringBuilder();
1612        String[] questionMarks = new String[addresses.size()];
1613
1614        whereArgs.addAll(addresses);
1615        Arrays.fill(questionMarks, "?");
1616        whereBuilder.append(Email.DATA1 + " IN (").
1617                append(TextUtils.join(",", questionMarks)).
1618                append(")");
1619
1620        ContentResolver resolver = context.getContentResolver();
1621        Cursor c = resolver.query(Email.CONTENT_URI,
1622                new String[]{Email.CONTACT_ID}, whereBuilder.toString(),
1623                whereArgs.toArray(new String[0]), null);
1624
1625        ArrayList<Long> contactIds = new ArrayList<Long>();
1626        if (c == null) {
1627            return contactIds;
1628        }
1629        try {
1630            while (c.moveToNext()) {
1631                contactIds.add(c.getLong(0));
1632            }
1633        } finally {
1634            c.close();
1635        }
1636        return contactIds;
1637    }
1638
1639    public static ContactIconInfo getContactInfo(
1640            final Context context, final String senderAddress,
1641            final int idealIconWidth, final int idealIconHeight,
1642            final int idealWearableBgWidth, final int idealWearableBgHeight) {
1643        final ContactIconInfo contactIconInfo = new ContactIconInfo();
1644        final List<Long> contactIds = findContacts( context, Arrays.asList(
1645                new String[] { senderAddress }));
1646
1647        if (contactIds != null) {
1648            for (final long id : contactIds) {
1649                final Uri contactUri = ContentUris.withAppendedId(
1650                        ContactsContract.Contacts.CONTENT_URI, id);
1651                final InputStream inputStream =
1652                        ContactsContract.Contacts.openContactPhotoInputStream(
1653                                context.getContentResolver(), contactUri, true /*preferHighres*/);
1654
1655                if (inputStream != null) {
1656                    try {
1657                        final Bitmap source = BitmapFactory.decodeStream(inputStream);
1658                        if (source != null) {
1659                            // We should scale this image to fit the intended size
1660                            contactIconInfo.icon = Bitmap.createScaledBitmap(source, idealIconWidth,
1661                                    idealIconHeight, true);
1662
1663                            contactIconInfo.wearableBg = Bitmap.createScaledBitmap(source,
1664                                    idealWearableBgWidth, idealWearableBgHeight, true);
1665                        }
1666
1667                        if (contactIconInfo.icon != null) {
1668                            break;
1669                        }
1670                    } finally {
1671                        Closeables.closeQuietly(inputStream);
1672                    }
1673                }
1674            }
1675        }
1676
1677        return contactIconInfo;
1678    }
1679
1680    private static String getMessageBodyWithoutElidedText(final Message message) {
1681        return getMessageBodyWithoutElidedText(message.getBodyAsHtml());
1682    }
1683
1684    public static String getMessageBodyWithoutElidedText(String html) {
1685        if (TextUtils.isEmpty(html)) {
1686            return "";
1687        }
1688        // Get the html "tree" for this message body
1689        final HtmlTree htmlTree = com.android.mail.utils.Utils.getHtmlTree(html);
1690        htmlTree.setPlainTextConverterFactory(MESSAGE_CONVERTER_FACTORY);
1691
1692        return htmlTree.getPlainText();
1693    }
1694
1695    public static void markSeen(final Context context, final Folder folder) {
1696        final Uri uri = folder.folderUri.fullUri;
1697
1698        final ContentValues values = new ContentValues(1);
1699        values.put(UIProvider.ConversationColumns.SEEN, 1);
1700
1701        context.getContentResolver().update(uri, values, null, null);
1702    }
1703
1704    /**
1705     * Returns a displayable string representing
1706     * the message sender. It has a preference toward showing the name,
1707     * but will fall back to the address if that is all that is available.
1708     */
1709    private static String getDisplayableSender(String sender) {
1710        final EmailAddress address = EmailAddress.getEmailAddress(sender);
1711
1712        String displayableSender = address.getName();
1713
1714        if (!TextUtils.isEmpty(displayableSender)) {
1715            return Address.decodeAddressPersonal(displayableSender);
1716        }
1717
1718        // If that fails, default to the sender address.
1719        displayableSender = address.getAddress();
1720
1721        // If we were unable to tokenize a name or address,
1722        // just use whatever was in the sender.
1723        if (TextUtils.isEmpty(displayableSender)) {
1724            displayableSender = sender;
1725        }
1726        return displayableSender;
1727    }
1728
1729    /**
1730     * Returns only the address portion of a message sender.
1731     */
1732    private static String getSenderAddress(String sender) {
1733        final EmailAddress address = EmailAddress.getEmailAddress(sender);
1734
1735        String tokenizedAddress = address.getAddress();
1736
1737        // If we were unable to tokenize a name or address,
1738        // just use whatever was in the sender.
1739        if (TextUtils.isEmpty(tokenizedAddress)) {
1740            tokenizedAddress = sender;
1741        }
1742        return tokenizedAddress;
1743    }
1744
1745    public static int getNotificationId(final android.accounts.Account account,
1746            final Folder folder) {
1747        return 1 ^ account.hashCode() ^ folder.hashCode();
1748    }
1749
1750    private static int getNotificationId(int summaryNotificationId, int childHashCode) {
1751        return summaryNotificationId ^ childHashCode;
1752    }
1753
1754    private static class NotificationKey {
1755        public final Account account;
1756        public final Folder folder;
1757
1758        public NotificationKey(Account account, Folder folder) {
1759            this.account = account;
1760            this.folder = folder;
1761        }
1762
1763        @Override
1764        public boolean equals(Object other) {
1765            if (!(other instanceof NotificationKey)) {
1766                return false;
1767            }
1768            NotificationKey key = (NotificationKey) other;
1769            return account.getAccountManagerAccount().equals(key.account.getAccountManagerAccount())
1770                    && folder.equals(key.folder);
1771        }
1772
1773        @Override
1774        public String toString() {
1775            return account.getDisplayName() + " " + folder.name;
1776        }
1777
1778        @Override
1779        public int hashCode() {
1780            final int accountHashCode = account.getAccountManagerAccount().hashCode();
1781            final int folderHashCode = folder.hashCode();
1782            return accountHashCode ^ folderHashCode;
1783        }
1784    }
1785
1786    /**
1787     * Contains the logic for converting the contents of one HtmlTree into
1788     * plaintext.
1789     */
1790    public static class MailMessagePlainTextConverter extends HtmlTree.DefaultPlainTextConverter {
1791        // Strings for parsing html message bodies
1792        private static final String ELIDED_TEXT_ELEMENT_NAME = "div";
1793        private static final String ELIDED_TEXT_ELEMENT_ATTRIBUTE_NAME = "class";
1794        private static final String ELIDED_TEXT_ELEMENT_ATTRIBUTE_CLASS_VALUE = "elided-text";
1795
1796        private static final HTML.Attribute ELIDED_TEXT_ATTRIBUTE =
1797                new HTML.Attribute(ELIDED_TEXT_ELEMENT_ATTRIBUTE_NAME, HTML.Attribute.NO_TYPE);
1798
1799        private static final HtmlDocument.Node ELIDED_TEXT_REPLACEMENT_NODE =
1800                HtmlDocument.createSelfTerminatingTag(HTML4.BR_ELEMENT, null, null, null);
1801
1802        private int mEndNodeElidedTextBlock = -1;
1803
1804        @Override
1805        public void addNode(HtmlDocument.Node n, int nodeNum, int endNum) {
1806            // If we are in the middle of an elided text block, don't add this node
1807            if (nodeNum < mEndNodeElidedTextBlock) {
1808                return;
1809            } else if (nodeNum == mEndNodeElidedTextBlock) {
1810                super.addNode(ELIDED_TEXT_REPLACEMENT_NODE, nodeNum, endNum);
1811                return;
1812            }
1813
1814            // If this tag starts another elided text block, we want to remember the end
1815            if (n instanceof HtmlDocument.Tag) {
1816                boolean foundElidedTextTag = false;
1817                final HtmlDocument.Tag htmlTag = (HtmlDocument.Tag)n;
1818                final HTML.Element htmlElement = htmlTag.getElement();
1819                if (ELIDED_TEXT_ELEMENT_NAME.equals(htmlElement.getName())) {
1820                    // Make sure that the class is what is expected
1821                    final List<HtmlDocument.TagAttribute> attributes =
1822                            htmlTag.getAttributes(ELIDED_TEXT_ATTRIBUTE);
1823                    for (HtmlDocument.TagAttribute attribute : attributes) {
1824                        if (ELIDED_TEXT_ELEMENT_ATTRIBUTE_CLASS_VALUE.equals(
1825                                attribute.getValue())) {
1826                            // Found an "elided-text" div.  Remember information about this tag
1827                            mEndNodeElidedTextBlock = endNum;
1828                            foundElidedTextTag = true;
1829                            break;
1830                        }
1831                    }
1832                }
1833
1834                if (foundElidedTextTag) {
1835                    return;
1836                }
1837            }
1838
1839            super.addNode(n, nodeNum, endNum);
1840        }
1841    }
1842
1843    /**
1844     * During account setup in Email, we may not have an inbox yet, so the notification setting had
1845     * to be stored in {@link AccountPreferences}. If it is still there, we need to move it to the
1846     * {@link FolderPreferences} now.
1847     */
1848    public static void moveNotificationSetting(final AccountPreferences accountPreferences,
1849            final FolderPreferences folderPreferences) {
1850        if (accountPreferences.isDefaultInboxNotificationsEnabledSet()) {
1851            // If this setting has been changed some other way, don't overwrite it
1852            if (!folderPreferences.isNotificationsEnabledSet()) {
1853                final boolean notificationsEnabled =
1854                        accountPreferences.getDefaultInboxNotificationsEnabled();
1855
1856                folderPreferences.setNotificationsEnabled(notificationsEnabled);
1857            }
1858
1859            accountPreferences.clearDefaultInboxNotificationsEnabled();
1860        }
1861    }
1862
1863    private static class NotificationBuilders {
1864        public final NotificationCompat.Builder notifBuilder;
1865        public final NotificationCompat.WearableExtender wearableNotifBuilder;
1866
1867        private NotificationBuilders(NotificationCompat.Builder notifBuilder,
1868                NotificationCompat.WearableExtender wearableNotifBuilder) {
1869            this.notifBuilder = notifBuilder;
1870            this.wearableNotifBuilder = wearableNotifBuilder;
1871        }
1872
1873        public static NotificationBuilders of(NotificationCompat.Builder notifBuilder,
1874                NotificationCompat.WearableExtender wearableNotifBuilder) {
1875            return new NotificationBuilders(notifBuilder, wearableNotifBuilder);
1876        }
1877    }
1878
1879    private static class ConfigResult {
1880        public String notificationTicker;
1881        public ContactIconInfo contactIconInfo;
1882    }
1883
1884    public static class ContactIconInfo {
1885        public Bitmap icon;
1886        public Bitmap wearableBg;
1887    }
1888}
1889