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