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