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