Home | History | Annotate | Download | only in datamodel
      1 /*
      2  * Copyright (C) 2015 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.messaging.datamodel;
     17 
     18 import android.app.Notification;
     19 import android.app.PendingIntent;
     20 import android.content.Context;
     21 import android.content.res.Resources;
     22 import android.database.Cursor;
     23 import android.graphics.Typeface;
     24 import android.net.Uri;
     25 import android.support.v4.app.NotificationCompat;
     26 import android.support.v4.app.NotificationCompat.Builder;
     27 import android.support.v4.app.NotificationCompat.WearableExtender;
     28 import android.support.v4.app.NotificationManagerCompat;
     29 import android.text.Html;
     30 import android.text.Spannable;
     31 import android.text.SpannableString;
     32 import android.text.SpannableStringBuilder;
     33 import android.text.Spanned;
     34 import android.text.TextUtils;
     35 import android.text.style.ForegroundColorSpan;
     36 import android.text.style.StyleSpan;
     37 import android.text.style.TextAppearanceSpan;
     38 import android.text.style.URLSpan;
     39 
     40 import com.android.messaging.Factory;
     41 import com.android.messaging.R;
     42 import com.android.messaging.datamodel.data.ConversationListItemData;
     43 import com.android.messaging.datamodel.data.ConversationMessageData;
     44 import com.android.messaging.datamodel.data.ConversationParticipantsData;
     45 import com.android.messaging.datamodel.data.MessageData;
     46 import com.android.messaging.datamodel.data.MessagePartData;
     47 import com.android.messaging.datamodel.data.ParticipantData;
     48 import com.android.messaging.datamodel.media.VideoThumbnailRequest;
     49 import com.android.messaging.sms.MmsUtils;
     50 import com.android.messaging.ui.UIIntents;
     51 import com.android.messaging.util.Assert;
     52 import com.android.messaging.util.AvatarUriUtil;
     53 import com.android.messaging.util.BugleGservices;
     54 import com.android.messaging.util.BugleGservicesKeys;
     55 import com.android.messaging.util.ContentType;
     56 import com.android.messaging.util.ConversationIdSet;
     57 import com.android.messaging.util.LogUtil;
     58 import com.android.messaging.util.PendingIntentConstants;
     59 import com.android.messaging.util.UriUtil;
     60 import com.google.common.collect.Lists;
     61 
     62 import java.util.ArrayList;
     63 import java.util.HashMap;
     64 import java.util.HashSet;
     65 import java.util.Iterator;
     66 import java.util.LinkedHashMap;
     67 import java.util.List;
     68 import java.util.Map;
     69 
     70 /**
     71  * Notification building class for conversation messages.
     72  *
     73  * Message Notifications are built in several stages with several utility classes.
     74  * 1) Perform a database query and fill a data structure with information on messages and
     75  *    conversations which need to be notified.
     76  * 2) Based on the data structure choose an appropriate NotificationState subclass to
     77  *    represent all the notifications.
     78  *    -- For one or more messages in one conversation: MultiMessageNotificationState.
     79  *    -- For multiple messages in multiple conversations: MultiConversationNotificationState
     80  *
     81  *  A three level structure is used to coalesce the data from the database. From bottom to top:
     82  *  1) NotificationLineInfo - A single message that needs to be notified.
     83  *  2) ConversationLineInfo - A list of NotificationLineInfo in a single conversation.
     84  *  3) ConversationInfoList - A list of ConversationLineInfo and the total number of messages.
     85  *
     86  *  The createConversationInfoList function performs the query and creates the data structure.
     87  */
     88 public abstract class MessageNotificationState extends NotificationState {
     89     // Logging
     90     static final String TAG = LogUtil.BUGLE_NOTIFICATIONS_TAG;
     91     private static final int MAX_MESSAGES_IN_WEARABLE_PAGE = 20;
     92 
     93     private static final int MAX_CHARACTERS_IN_GROUP_NAME = 30;
     94 
     95     private static final int REPLY_INTENT_REQUEST_CODE_OFFSET = 0;
     96     private static final int NUM_EXTRA_REQUEST_CODES_NEEDED = 1;
     97     protected String mTickerSender = null;
     98     protected CharSequence mTickerText = null;
     99     protected String mTitle = null;
    100     protected CharSequence mContent = null;
    101     protected Uri mAttachmentUri = null;
    102     protected String mAttachmentType = null;
    103     protected boolean mTickerNoContent;
    104 
    105     @Override
    106     protected Uri getAttachmentUri() {
    107         return mAttachmentUri;
    108     }
    109 
    110     @Override
    111     protected String getAttachmentType() {
    112         return mAttachmentType;
    113     }
    114 
    115     @Override
    116     public int getIcon() {
    117         return R.drawable.ic_sms_light;
    118     }
    119 
    120     @Override
    121     public int getPriority() {
    122         // Returning PRIORITY_HIGH causes L to put up a HUD notification. Without it, the ticker
    123         // isn't displayed.
    124         return Notification.PRIORITY_HIGH;
    125     }
    126 
    127     /**
    128      * Base class for single notification events for messages. Multiple of these
    129      * may be grouped into a single conversation.
    130      */
    131     static class NotificationLineInfo {
    132 
    133         final int mNotificationType;
    134 
    135         NotificationLineInfo() {
    136             mNotificationType = BugleNotifications.LOCAL_SMS_NOTIFICATION;
    137         }
    138 
    139         NotificationLineInfo(final int notificationType) {
    140             mNotificationType = notificationType;
    141         }
    142     }
    143 
    144     /**
    145      * Information on a single chat message which should be shown in a notification.
    146      */
    147     static class MessageLineInfo extends NotificationLineInfo {
    148         final CharSequence mText;
    149         Uri mAttachmentUri;
    150         String mAttachmentType;
    151         final String mAuthorFullName;
    152         final String mAuthorFirstName;
    153         boolean mIsManualDownloadNeeded;
    154         final String mMessageId;
    155 
    156         MessageLineInfo(final boolean isGroup, final String authorFullName,
    157                 final String authorFirstName, final CharSequence text, final Uri attachmentUrl,
    158                 final String attachmentType, final boolean isManualDownloadNeeded,
    159                 final String messageId) {
    160             super(BugleNotifications.LOCAL_SMS_NOTIFICATION);
    161             mAuthorFullName = authorFullName;
    162             mAuthorFirstName = authorFirstName;
    163             mText = text;
    164             mAttachmentUri = attachmentUrl;
    165             mAttachmentType = attachmentType;
    166             mIsManualDownloadNeeded = isManualDownloadNeeded;
    167             mMessageId = messageId;
    168         }
    169     }
    170 
    171     /**
    172      * Information on all the notification messages within a single conversation.
    173      */
    174     static class ConversationLineInfo {
    175         // Conversation id of the latest message in the notification for this merged conversation.
    176         final String mConversationId;
    177 
    178         // True if this represents a group conversation.
    179         final boolean mIsGroup;
    180 
    181         // Name of the group conversation if available.
    182         final String mGroupConversationName;
    183 
    184         // True if this conversation's recipients includes one or more email address(es)
    185         // (see ConversationColumns.INCLUDE_EMAIL_ADDRESS)
    186         final boolean mIncludeEmailAddress;
    187 
    188         // Timestamp of the latest message
    189         final long mReceivedTimestamp;
    190 
    191         // Self participant id.
    192         final String mSelfParticipantId;
    193 
    194         // List of individual line notifications to be parsed later.
    195         final List<NotificationLineInfo> mLineInfos;
    196 
    197         // Total number of messages. Might be different that mLineInfos.size() as the number of
    198         // line infos is capped.
    199         int mTotalMessageCount;
    200 
    201         // Custom ringtone if set
    202         final String mRingtoneUri;
    203 
    204         // Should notification be enabled for this conversation?
    205         final boolean mNotificationEnabled;
    206 
    207         // Should notifications vibrate for this conversation?
    208         final boolean mNotificationVibrate;
    209 
    210         // Avatar uri of sender
    211         final Uri mAvatarUri;
    212 
    213         // Contact uri of sender
    214         final Uri mContactUri;
    215 
    216         // Subscription id.
    217         final int mSubId;
    218 
    219         // Number of participants
    220         final int mParticipantCount;
    221 
    222         public ConversationLineInfo(final String conversationId,
    223                 final boolean isGroup,
    224                 final String groupConversationName,
    225                 final boolean includeEmailAddress,
    226                 final long receivedTimestamp,
    227                 final String selfParticipantId,
    228                 final String ringtoneUri,
    229                 final boolean notificationEnabled,
    230                 final boolean notificationVibrate,
    231                 final Uri avatarUri,
    232                 final Uri contactUri,
    233                 final int subId,
    234                 final int participantCount) {
    235             mConversationId = conversationId;
    236             mIsGroup = isGroup;
    237             mGroupConversationName = groupConversationName;
    238             mIncludeEmailAddress = includeEmailAddress;
    239             mReceivedTimestamp = receivedTimestamp;
    240             mSelfParticipantId = selfParticipantId;
    241             mLineInfos = new ArrayList<NotificationLineInfo>();
    242             mTotalMessageCount = 0;
    243             mRingtoneUri = ringtoneUri;
    244             mAvatarUri = avatarUri;
    245             mContactUri = contactUri;
    246             mNotificationEnabled = notificationEnabled;
    247             mNotificationVibrate = notificationVibrate;
    248             mSubId = subId;
    249             mParticipantCount = participantCount;
    250         }
    251 
    252         public int getLatestMessageNotificationType() {
    253             final MessageLineInfo messageLineInfo = getLatestMessageLineInfo();
    254             if (messageLineInfo == null) {
    255                 return BugleNotifications.LOCAL_SMS_NOTIFICATION;
    256             }
    257             return messageLineInfo.mNotificationType;
    258         }
    259 
    260         public String getLatestMessageId() {
    261             final MessageLineInfo messageLineInfo = getLatestMessageLineInfo();
    262             if (messageLineInfo == null) {
    263                 return null;
    264             }
    265             return messageLineInfo.mMessageId;
    266         }
    267 
    268         public boolean getDoesLatestMessageNeedDownload() {
    269             final MessageLineInfo messageLineInfo = getLatestMessageLineInfo();
    270             if (messageLineInfo == null) {
    271                 return false;
    272             }
    273             return messageLineInfo.mIsManualDownloadNeeded;
    274         }
    275 
    276         private MessageLineInfo getLatestMessageLineInfo() {
    277             // The latest message is stored at index zero of the message line infos.
    278             if (mLineInfos.size() > 0 && mLineInfos.get(0) instanceof MessageLineInfo) {
    279                 return (MessageLineInfo) mLineInfos.get(0);
    280             }
    281             return null;
    282         }
    283     }
    284 
    285     /**
    286      * Information on all the notification messages across all conversations.
    287      */
    288     public static class ConversationInfoList {
    289         final int mMessageCount;
    290         final List<ConversationLineInfo> mConvInfos;
    291         public ConversationInfoList(final int count, final List<ConversationLineInfo> infos) {
    292             mMessageCount = count;
    293             mConvInfos = infos;
    294         }
    295     }
    296 
    297     final ConversationInfoList mConvList;
    298     private long mLatestReceivedTimestamp;
    299 
    300     private static ConversationIdSet makeConversationIdSet(final ConversationInfoList convList) {
    301         ConversationIdSet set = null;
    302         if (convList != null && convList.mConvInfos != null && convList.mConvInfos.size() > 0) {
    303             set = new ConversationIdSet();
    304             for (final ConversationLineInfo info : convList.mConvInfos) {
    305                     set.add(info.mConversationId);
    306             }
    307         }
    308         return set;
    309     }
    310 
    311     protected MessageNotificationState(final ConversationInfoList convList) {
    312         super(makeConversationIdSet(convList));
    313         mConvList = convList;
    314         mType = PendingIntentConstants.SMS_NOTIFICATION_ID;
    315         mLatestReceivedTimestamp = Long.MIN_VALUE;
    316         if (convList != null) {
    317             for (final ConversationLineInfo info : convList.mConvInfos) {
    318                 mLatestReceivedTimestamp = Math.max(mLatestReceivedTimestamp,
    319                         info.mReceivedTimestamp);
    320             }
    321         }
    322     }
    323 
    324     @Override
    325     public long getLatestReceivedTimestamp() {
    326         return mLatestReceivedTimestamp;
    327     }
    328 
    329     @Override
    330     public int getNumRequestCodesNeeded() {
    331         // Get additional request codes for the Reply PendingIntent (wearables only)
    332         // and the DND PendingIntent.
    333         return super.getNumRequestCodesNeeded() + NUM_EXTRA_REQUEST_CODES_NEEDED;
    334     }
    335 
    336     private int getBaseExtraRequestCode() {
    337         return mBaseRequestCode + super.getNumRequestCodesNeeded();
    338     }
    339 
    340     public int getReplyIntentRequestCode() {
    341         return getBaseExtraRequestCode() + REPLY_INTENT_REQUEST_CODE_OFFSET;
    342     }
    343 
    344     @Override
    345     public PendingIntent getClearIntent() {
    346         return UIIntents.get().getPendingIntentForClearingNotifications(
    347                     Factory.get().getApplicationContext(),
    348                     BugleNotifications.UPDATE_MESSAGES,
    349                     mConversationIds,
    350                     getClearIntentRequestCode());
    351     }
    352 
    353     /**
    354      * Notification for multiple messages in at least 2 different conversations.
    355      */
    356     public static class MultiConversationNotificationState extends MessageNotificationState {
    357 
    358         public final List<MessageNotificationState>
    359                 mChildren = new ArrayList<MessageNotificationState>();
    360 
    361         public MultiConversationNotificationState(
    362                 final ConversationInfoList convList, final MessageNotificationState state) {
    363             super(convList);
    364             mAttachmentUri = null;
    365             mAttachmentType = null;
    366 
    367             // Pull the ticker title/text from the single notification
    368             mTickerSender = state.getTitle();
    369             mTitle = Factory.get().getApplicationContext().getResources().getQuantityString(
    370                     R.plurals.notification_new_messages,
    371                     convList.mMessageCount, convList.mMessageCount);
    372             mTickerText = state.mContent;
    373 
    374             // Create child notifications for each conversation,
    375             // which will be displayed (only) on a wearable device.
    376             for (int i = 0; i < convList.mConvInfos.size(); i++) {
    377                 final ConversationLineInfo convInfo = convList.mConvInfos.get(i);
    378                 if (!(convInfo.mLineInfos.get(0) instanceof MessageLineInfo)) {
    379                     continue;
    380                 }
    381                 setPeopleForConversation(convInfo.mConversationId);
    382                 final ConversationInfoList list = new ConversationInfoList(
    383                         convInfo.mTotalMessageCount, Lists.newArrayList(convInfo));
    384                 mChildren.add(new BundledMessageNotificationState(list, i));
    385             }
    386         }
    387 
    388         @Override
    389         public int getIcon() {
    390             return R.drawable.ic_sms_multi_light;
    391         }
    392 
    393         @Override
    394         protected NotificationCompat.Style build(final Builder builder) {
    395             builder.setContentTitle(mTitle);
    396             NotificationCompat.InboxStyle inboxStyle = null;
    397             inboxStyle = new NotificationCompat.InboxStyle(builder);
    398 
    399             final Context context = Factory.get().getApplicationContext();
    400             // enumeration_comma is defined as ", "
    401             final String separator = context.getString(R.string.enumeration_comma);
    402             final StringBuilder senders = new StringBuilder();
    403             long when = 0;
    404             for (int i = 0; i < mConvList.mConvInfos.size(); i++) {
    405                 final ConversationLineInfo convInfo = mConvList.mConvInfos.get(i);
    406                 if (convInfo.mReceivedTimestamp > when) {
    407                     when = convInfo.mReceivedTimestamp;
    408                 }
    409                 String sender;
    410                 CharSequence text;
    411                 final NotificationLineInfo lineInfo = convInfo.mLineInfos.get(0);
    412                 final MessageLineInfo messageLineInfo = (MessageLineInfo) lineInfo;
    413                 if (convInfo.mIsGroup) {
    414                     sender = (convInfo.mGroupConversationName.length() >
    415                             MAX_CHARACTERS_IN_GROUP_NAME) ?
    416                                     truncateGroupMessageName(convInfo.mGroupConversationName)
    417                                     : convInfo.mGroupConversationName;
    418                 } else {
    419                     sender = messageLineInfo.mAuthorFullName;
    420                 }
    421                 text = messageLineInfo.mText;
    422                 mAttachmentUri = messageLineInfo.mAttachmentUri;
    423                 mAttachmentType = messageLineInfo.mAttachmentType;
    424 
    425                 inboxStyle.addLine(BugleNotifications.formatInboxMessage(
    426                         sender, text, mAttachmentUri, mAttachmentType));
    427                 if (sender != null) {
    428                     if (senders.length() > 0) {
    429                         senders.append(separator);
    430                     }
    431                     senders.append(sender);
    432                 }
    433             }
    434             // for collapsed state
    435             mContent = senders;
    436             builder.setContentText(senders)
    437                 .setTicker(getTicker())
    438                 .setWhen(when);
    439 
    440             return inboxStyle;
    441         }
    442     }
    443 
    444     /**
    445      * Truncate group conversation name to be displayed in the notifications. This either truncates
    446      * the entire group name or finds the last comma in the available length and truncates the name
    447      * at that point
    448      */
    449     private static String truncateGroupMessageName(final String conversationName) {
    450         int endIndex = MAX_CHARACTERS_IN_GROUP_NAME;
    451         for (int i = MAX_CHARACTERS_IN_GROUP_NAME; i >= 0; i--) {
    452             // The dividing marker should stay consistent with ConversationListItemData.DIVIDER_TEXT
    453             if (conversationName.charAt(i) == ',') {
    454                 endIndex = i;
    455                 break;
    456             }
    457         }
    458         return conversationName.substring(0, endIndex) + '\u2026';
    459     }
    460 
    461     /**
    462      * Notification for multiple messages in a single conversation. Also used if there is a single
    463      * message in a single conversation.
    464      */
    465     public static class MultiMessageNotificationState extends MessageNotificationState {
    466 
    467         public MultiMessageNotificationState(final ConversationInfoList convList) {
    468             super(convList);
    469             // This conversation has been accepted.
    470             final ConversationLineInfo convInfo = convList.mConvInfos.get(0);
    471             setAvatarUrlsForConversation(convInfo.mConversationId);
    472             setPeopleForConversation(convInfo.mConversationId);
    473 
    474             final Context context = Factory.get().getApplicationContext();
    475             MessageLineInfo messageInfo = (MessageLineInfo) convInfo.mLineInfos.get(0);
    476             // attached photo
    477             mAttachmentUri = messageInfo.mAttachmentUri;
    478             mAttachmentType = messageInfo.mAttachmentType;
    479             mContent = messageInfo.mText;
    480 
    481             if (mAttachmentUri != null) {
    482                 // The default attachment type is an image, since that's what was originally
    483                 // supported. When there's no content type, assume it's an image.
    484                 int message = R.string.notification_picture;
    485                 if (ContentType.isAudioType(mAttachmentType)) {
    486                     message = R.string.notification_audio;
    487                 } else if (ContentType.isVideoType(mAttachmentType)) {
    488                     message = R.string.notification_video;
    489                 } else if (ContentType.isVCardType(mAttachmentType)) {
    490                     message = R.string.notification_vcard;
    491                 }
    492                 final String attachment = context.getString(message);
    493                 final SpannableStringBuilder spanBuilder = new SpannableStringBuilder();
    494                 if (!TextUtils.isEmpty(mContent)) {
    495                     spanBuilder.append(mContent).append(System.getProperty("line.separator"));
    496                 }
    497                 final int start = spanBuilder.length();
    498                 spanBuilder.append(attachment);
    499                 spanBuilder.setSpan(new StyleSpan(Typeface.ITALIC), start, spanBuilder.length(),
    500                         Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    501                 mContent = spanBuilder;
    502             }
    503             if (convInfo.mIsGroup) {
    504                 // When the message is part of a group, the sender's first name
    505                 // is prepended to the message, but not for the ticker message.
    506                 mTickerText = mContent;
    507                 mTickerSender = messageInfo.mAuthorFullName;
    508                 // append the bold name to the front of the message
    509                 mContent = BugleNotifications.buildSpaceSeparatedMessage(
    510                         messageInfo.mAuthorFullName, mContent, mAttachmentUri,
    511                         mAttachmentType);
    512                 mTitle = convInfo.mGroupConversationName;
    513             } else {
    514                 // No matter how many messages there are, since this is a 1:1, just
    515                 // get the author full name from the first one.
    516                 messageInfo = (MessageLineInfo) convInfo.mLineInfos.get(0);
    517                 mTitle = messageInfo.mAuthorFullName;
    518             }
    519         }
    520 
    521         @Override
    522         protected NotificationCompat.Style build(final Builder builder) {
    523             builder.setContentTitle(mTitle)
    524                 .setTicker(getTicker());
    525 
    526             NotificationCompat.Style notifStyle = null;
    527             final ConversationLineInfo convInfo = mConvList.mConvInfos.get(0);
    528             final List<NotificationLineInfo> lineInfos = convInfo.mLineInfos;
    529             final int messageCount = lineInfos.size();
    530             // At this point, all the messages come from the same conversation. We need to load
    531             // the sender's avatar and then finish building the notification on a callback.
    532 
    533             builder.setContentText(mContent);   // for collapsed state
    534 
    535             if (messageCount == 1) {
    536                 final boolean shouldShowImage = ContentType.isImageType(mAttachmentType)
    537                         || (ContentType.isVideoType(mAttachmentType)
    538                         && VideoThumbnailRequest.shouldShowIncomingVideoThumbnails());
    539                 if (mAttachmentUri != null && shouldShowImage) {
    540                     // Show "Picture" as the content
    541                     final MessageLineInfo messageLineInfo = (MessageLineInfo) lineInfos.get(0);
    542                     String authorFirstName = messageLineInfo.mAuthorFirstName;
    543 
    544                     // For the collapsed state, just show "picture" unless this is a
    545                     // group conversation. If it's a group, show the sender name and
    546                     // "picture".
    547                     final CharSequence tickerTag =
    548                             BugleNotifications.formatAttachmentTag(authorFirstName,
    549                                     mAttachmentType);
    550                     // For 1:1 notifications don't show first name in the notification, but
    551                     // do show it in the ticker text
    552                     CharSequence pictureTag = tickerTag;
    553                     if (!convInfo.mIsGroup) {
    554                         authorFirstName = null;
    555                         pictureTag = BugleNotifications.formatAttachmentTag(authorFirstName,
    556                                 mAttachmentType);
    557                     }
    558                     builder.setContentText(pictureTag);
    559                     builder.setTicker(tickerTag);
    560 
    561                     notifStyle = new NotificationCompat.BigPictureStyle(builder)
    562                         .setSummaryText(BugleNotifications.formatInboxMessage(
    563                                 authorFirstName,
    564                                 null, null,
    565                                 null));  // expanded state, just show sender
    566                 } else {
    567                     notifStyle = new NotificationCompat.BigTextStyle(builder)
    568                     .bigText(mContent);
    569                 }
    570             } else {
    571                 // We've got multiple messages for the same sender.
    572                 // Starting with the oldest new message, display the full text of each message.
    573                 // Begin a line for each subsequent message.
    574                 final SpannableStringBuilder buf = new SpannableStringBuilder();
    575 
    576                 for (int i = lineInfos.size() - 1; i >= 0; --i) {
    577                     final NotificationLineInfo info = lineInfos.get(i);
    578                     final MessageLineInfo messageLineInfo = (MessageLineInfo) info;
    579                     mAttachmentUri = messageLineInfo.mAttachmentUri;
    580                     mAttachmentType = messageLineInfo.mAttachmentType;
    581                     CharSequence text = messageLineInfo.mText;
    582                     if (!TextUtils.isEmpty(text) || mAttachmentUri != null) {
    583                         if (convInfo.mIsGroup) {
    584                             // append the bold name to the front of the message
    585                             text = BugleNotifications.buildSpaceSeparatedMessage(
    586                                     messageLineInfo.mAuthorFullName, text, mAttachmentUri,
    587                                     mAttachmentType);
    588                         } else {
    589                             text = BugleNotifications.buildSpaceSeparatedMessage(
    590                                     null, text, mAttachmentUri, mAttachmentType);
    591                         }
    592                         buf.append(text);
    593                         if (i > 0) {
    594                             buf.append('\n');
    595                         }
    596                     }
    597                 }
    598 
    599                 // Show a single notification -- big style with the text of all the messages
    600                 notifStyle = new NotificationCompat.BigTextStyle(builder).bigText(buf);
    601             }
    602             builder.setWhen(convInfo.mReceivedTimestamp);
    603             return notifStyle;
    604         }
    605 
    606     }
    607 
    608     private static boolean firstNameUsedMoreThanOnce(
    609             final HashMap<String, Integer> map, final String firstName) {
    610         if (map == null) {
    611             return false;
    612         }
    613         if (firstName == null) {
    614             return false;
    615         }
    616         final Integer count = map.get(firstName);
    617         if (count != null) {
    618             return count > 1;
    619         } else {
    620             return false;
    621         }
    622     }
    623 
    624     private static HashMap<String, Integer> scanFirstNames(final String conversationId) {
    625         final Context context = Factory.get().getApplicationContext();
    626         final Uri uri =
    627                 MessagingContentProvider.buildConversationParticipantsUri(conversationId);
    628         final Cursor participantsCursor = context.getContentResolver().query(
    629                 uri, ParticipantData.ParticipantsQuery.PROJECTION, null, null, null);
    630         final ConversationParticipantsData participantsData = new ConversationParticipantsData();
    631         participantsData.bind(participantsCursor);
    632         final Iterator<ParticipantData> iter = participantsData.iterator();
    633 
    634         final HashMap<String, Integer> firstNames = new HashMap<String, Integer>();
    635         boolean seenSelf = false;
    636         while (iter.hasNext()) {
    637             final ParticipantData participant = iter.next();
    638             // Make sure we only add the self participant once
    639             if (participant.isSelf()) {
    640                 if (seenSelf) {
    641                     continue;
    642                 } else {
    643                     seenSelf = true;
    644                 }
    645             }
    646 
    647             final String firstName = participant.getFirstName();
    648             if (firstName == null) {
    649                 continue;
    650             }
    651 
    652             final int currentCount = firstNames.containsKey(firstName)
    653                     ? firstNames.get(firstName)
    654                     : 0;
    655             firstNames.put(firstName, currentCount + 1);
    656         }
    657         return firstNames;
    658     }
    659 
    660     // Essentially, we're building a list of the past 20 messages for this conversation to display
    661     // on the wearable.
    662     public static Notification buildConversationPageForWearable(final String conversationId,
    663             int participantCount) {
    664         final Context context = Factory.get().getApplicationContext();
    665 
    666         // Limit the number of messages to show. We just want enough to provide context for the
    667         // notification. Fetch one more than we need, so we can tell if there are more messages
    668         // before the one we're showing.
    669         // TODO: in the query, a multipart message will contain a row for each part.
    670         // We might need a smarter GROUP_BY. On the other hand, we might want to show each of the
    671         // parts as separate messages on the wearable.
    672         final int limit = MAX_MESSAGES_IN_WEARABLE_PAGE + 1;
    673 
    674         final List<CharSequence> messages = Lists.newArrayList();
    675         boolean hasSeenMessagesBeforeNotification = false;
    676         Cursor convMessageCursor = null;
    677         try {
    678             final DatabaseWrapper db = DataModel.get().getDatabase();
    679 
    680             final String[] queryArgs = { conversationId };
    681             final String convPageSql = ConversationMessageData.getWearableQuerySql() + " LIMIT " +
    682                     limit;
    683             convMessageCursor = db.rawQuery(
    684                     convPageSql,
    685                     queryArgs);
    686 
    687             if (convMessageCursor == null || !convMessageCursor.moveToFirst()) {
    688                 return null;
    689             }
    690             final ConversationMessageData convMessageData =
    691                     new ConversationMessageData();
    692 
    693             final HashMap<String, Integer> firstNames = scanFirstNames(conversationId);
    694             do {
    695                 convMessageData.bind(convMessageCursor);
    696 
    697                 final String authorFullName = convMessageData.getSenderFullName();
    698                 final String authorFirstName = convMessageData.getSenderFirstName();
    699                 String text = convMessageData.getText();
    700 
    701                 final boolean isSmsPushNotification = convMessageData.getIsMmsNotification();
    702 
    703                 // if auto-download was off to show a message to tap to download the message. We
    704                 // might need to get that working again.
    705                 if (isSmsPushNotification && text != null) {
    706                     text = convertHtmlAndStripUrls(text).toString();
    707                 }
    708                 // Skip messages without any content
    709                 if (TextUtils.isEmpty(text) && !convMessageData.hasAttachments()) {
    710                     continue;
    711                 }
    712                 // Track whether there are messages prior to the one(s) shown in the notification.
    713                 if (convMessageData.getIsSeen()) {
    714                     hasSeenMessagesBeforeNotification = true;
    715                 }
    716 
    717                 final boolean usedMoreThanOnce = firstNameUsedMoreThanOnce(
    718                         firstNames, authorFirstName);
    719                 String displayName = usedMoreThanOnce ? authorFullName : authorFirstName;
    720                 if (TextUtils.isEmpty(displayName)) {
    721                     if (convMessageData.getIsIncoming()) {
    722                         displayName = convMessageData.getSenderDisplayDestination();
    723                         if (TextUtils.isEmpty(displayName)) {
    724                             displayName = context.getString(R.string.unknown_sender);
    725                         }
    726                     } else {
    727                         displayName = context.getString(R.string.unknown_self_participant);
    728                     }
    729                 }
    730 
    731                 Uri attachmentUri = null;
    732                 String attachmentType = null;
    733                 final List<MessagePartData> attachments = convMessageData.getAttachments();
    734                 for (final MessagePartData messagePartData : attachments) {
    735                     // Look for the first attachment that's not the text piece.
    736                     if (!messagePartData.isText()) {
    737                         attachmentUri = messagePartData.getContentUri();
    738                         attachmentType = messagePartData.getContentType();
    739                         break;
    740                     }
    741                 }
    742 
    743                 final CharSequence message = BugleNotifications.buildSpaceSeparatedMessage(
    744                         displayName, text, attachmentUri, attachmentType);
    745                 messages.add(message);
    746 
    747             } while (convMessageCursor.moveToNext());
    748         } finally {
    749             if (convMessageCursor != null) {
    750                 convMessageCursor.close();
    751             }
    752         }
    753 
    754         // If there is no conversation history prior to what is already visible in the main
    755         // notification, there's no need to include the conversation log, too.
    756         final int maxMessagesInNotification = getMaxMessagesInConversationNotification();
    757         if (!hasSeenMessagesBeforeNotification && messages.size() <= maxMessagesInNotification) {
    758             return null;
    759         }
    760 
    761         final SpannableStringBuilder bigText = new SpannableStringBuilder();
    762         // There is at least 1 message prior to the first one that we're going to show.
    763         // Indicate this by inserting an ellipsis at the beginning of the conversation log.
    764         if (convMessageCursor.getCount() == limit) {
    765             bigText.append(context.getString(R.string.ellipsis) + "\n\n");
    766             if (messages.size() > MAX_MESSAGES_IN_WEARABLE_PAGE) {
    767                 messages.remove(messages.size() - 1);
    768             }
    769         }
    770         // Messages are sorted in descending timestamp order, so iterate backwards
    771         // to get them back in ascending order for display purposes.
    772         for (int i = messages.size() - 1; i >= 0; --i) {
    773             bigText.append(messages.get(i));
    774             if (i > 0) {
    775                 bigText.append("\n\n");
    776             }
    777         }
    778         ++participantCount;     // Add in myself
    779 
    780         if (participantCount > 2) {
    781             final SpannableString statusText = new SpannableString(
    782                     context.getResources().getQuantityString(R.plurals.wearable_participant_count,
    783                             participantCount, participantCount));
    784             statusText.setSpan(new ForegroundColorSpan(context.getResources().getColor(
    785                     R.color.wearable_notification_participants_count)), 0, statusText.length(),
    786                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    787             bigText.append("\n\n").append(statusText);
    788         }
    789 
    790         final NotificationCompat.Builder notifBuilder = new NotificationCompat.Builder(context);
    791         final NotificationCompat.Style notifStyle =
    792                 new NotificationCompat.BigTextStyle(notifBuilder).bigText(bigText);
    793         notifBuilder.setStyle(notifStyle);
    794 
    795         final WearableExtender wearableExtender = new WearableExtender();
    796         wearableExtender.setStartScrollBottom(true);
    797         notifBuilder.extend(wearableExtender);
    798 
    799         return notifBuilder.build();
    800     }
    801 
    802     /**
    803      * Notification for one or more messages in a single conversation, which is bundled together
    804      * with notifications for other conversations on a wearable device.
    805      */
    806     public static class BundledMessageNotificationState extends MultiMessageNotificationState {
    807         public int mGroupOrder;
    808         public BundledMessageNotificationState(final ConversationInfoList convList,
    809                 final int groupOrder) {
    810             super(convList);
    811             mGroupOrder = groupOrder;
    812         }
    813     }
    814 
    815     /**
    816      * Performs a query on the database.
    817      */
    818     private static ConversationInfoList createConversationInfoList() {
    819         // Map key is conversation id. We use LinkedHashMap to ensure that entries are iterated in
    820         // the same order they were originally added. We scan unseen messages from newest to oldest,
    821         // so the corresponding conversations are added in that order, too.
    822         final Map<String, ConversationLineInfo> convLineInfos = new LinkedHashMap<>();
    823         int messageCount = 0;
    824 
    825         Cursor convMessageCursor = null;
    826         try {
    827             final Context context = Factory.get().getApplicationContext();
    828             final DatabaseWrapper db = DataModel.get().getDatabase();
    829 
    830             convMessageCursor = db.rawQuery(
    831                     ConversationMessageData.getNotificationQuerySql(),
    832                     null);
    833 
    834             if (convMessageCursor != null && convMessageCursor.moveToFirst()) {
    835                 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
    836                     LogUtil.v(TAG, "MessageNotificationState: Found unseen message notifications.");
    837                 }
    838                 final ConversationMessageData convMessageData =
    839                         new ConversationMessageData();
    840 
    841                 HashMap<String, Integer> firstNames = null;
    842                 String conversationIdForFirstNames = null;
    843                 String groupConversationName = null;
    844                 final int maxMessages = getMaxMessagesInConversationNotification();
    845 
    846                 do {
    847                     convMessageData.bind(convMessageCursor);
    848 
    849                     // First figure out if this is a valid message.
    850                     String authorFullName = convMessageData.getSenderFullName();
    851                     String authorFirstName = convMessageData.getSenderFirstName();
    852                     final String messageText = convMessageData.getText();
    853 
    854                     final String convId = convMessageData.getConversationId();
    855                     final String messageId = convMessageData.getMessageId();
    856 
    857                     CharSequence text = messageText;
    858                     final boolean isManualDownloadNeeded = convMessageData.getIsMmsNotification();
    859                     if (isManualDownloadNeeded) {
    860                         // Don't try and convert the text from html if it's sms and not a sms push
    861                         // notification.
    862                         Assert.equals(MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD,
    863                                 convMessageData.getStatus());
    864                         text = context.getResources().getString(
    865                                 R.string.message_title_manual_download);
    866                     }
    867                     ConversationLineInfo currConvInfo = convLineInfos.get(convId);
    868                     if (currConvInfo == null) {
    869                         final ConversationListItemData convData =
    870                                 ConversationListItemData.getExistingConversation(db, convId);
    871                         if (!convData.getNotificationEnabled()) {
    872                             // Skip conversations that have notifications disabled.
    873                             continue;
    874                         }
    875                         final int subId = BugleDatabaseOperations.getSelfSubscriptionId(db,
    876                                 convData.getSelfId());
    877                         groupConversationName = convData.getName();
    878                         final Uri avatarUri = AvatarUriUtil.createAvatarUri(
    879                                 convMessageData.getSenderProfilePhotoUri(),
    880                                 convMessageData.getSenderFullName(),
    881                                 convMessageData.getSenderNormalizedDestination(),
    882                                 convMessageData.getSenderContactLookupKey());
    883                         currConvInfo = new ConversationLineInfo(convId,
    884                                 convData.getIsGroup(),
    885                                 groupConversationName,
    886                                 convData.getIncludeEmailAddress(),
    887                                 convMessageData.getReceivedTimeStamp(),
    888                                 convData.getSelfId(),
    889                                 convData.getNotificationSoundUri(),
    890                                 convData.getNotificationEnabled(),
    891                                 convData.getNotifiationVibrate(),
    892                                 avatarUri,
    893                                 convMessageData.getSenderContactLookupUri(),
    894                                 subId,
    895                                 convData.getParticipantCount());
    896                         convLineInfos.put(convId, currConvInfo);
    897                     }
    898                     // Prepare the message line
    899                     if (currConvInfo.mTotalMessageCount < maxMessages) {
    900                         if (currConvInfo.mIsGroup) {
    901                             if (authorFirstName == null) {
    902                                 // authorFullName might be null as well. In that case, we won't
    903                                 // show an author. That is better than showing all the group
    904                                 // names again on the 2nd line.
    905                                 authorFirstName = authorFullName;
    906                             }
    907                         } else {
    908                             // don't recompute this if we don't need to
    909                             if (!TextUtils.equals(conversationIdForFirstNames, convId)) {
    910                                 firstNames = scanFirstNames(convId);
    911                                 conversationIdForFirstNames = convId;
    912                             }
    913                             if (firstNames != null) {
    914                                 final Integer count = firstNames.get(authorFirstName);
    915                                 if (count != null && count > 1) {
    916                                     authorFirstName = authorFullName;
    917                                 }
    918                             }
    919 
    920                             if (authorFullName == null) {
    921                                 authorFullName = groupConversationName;
    922                             }
    923                             if (authorFirstName == null) {
    924                                 authorFirstName = groupConversationName;
    925                             }
    926                         }
    927                         final String subjectText = MmsUtils.cleanseMmsSubject(
    928                                 context.getResources(),
    929                                 convMessageData.getMmsSubject());
    930                         if (!TextUtils.isEmpty(subjectText)) {
    931                             final String subjectLabel =
    932                                     context.getString(R.string.subject_label);
    933                             final SpannableStringBuilder spanBuilder =
    934                                     new SpannableStringBuilder();
    935 
    936                             spanBuilder.append(context.getString(R.string.notification_subject,
    937                                     subjectLabel, subjectText));
    938                             spanBuilder.setSpan(new TextAppearanceSpan(
    939                                     context, R.style.NotificationSubjectText), 0,
    940                                     subjectLabel.length(),
    941                                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    942                             if (!TextUtils.isEmpty(text)) {
    943                                 // Now add the actual message text below the subject header.
    944                                 spanBuilder.append(System.getProperty("line.separator") + text);
    945                             }
    946                             text = spanBuilder;
    947                         }
    948                         // If we've got attachments, find the best one. If one of the messages is
    949                         // a photo, save the url so we'll display a big picture notification.
    950                         // Otherwise, show the first one we find.
    951                         Uri attachmentUri = null;
    952                         String attachmentType = null;
    953                         final MessagePartData messagePartData =
    954                                 getMostInterestingAttachment(convMessageData);
    955                         if (messagePartData != null) {
    956                             attachmentUri = messagePartData.getContentUri();
    957                             attachmentType = messagePartData.getContentType();
    958                         }
    959                         currConvInfo.mLineInfos.add(new MessageLineInfo(currConvInfo.mIsGroup,
    960                                 authorFullName, authorFirstName, text,
    961                                 attachmentUri, attachmentType, isManualDownloadNeeded, messageId));
    962                     }
    963                     messageCount++;
    964                     currConvInfo.mTotalMessageCount++;
    965                 } while (convMessageCursor.moveToNext());
    966             }
    967         } finally {
    968             if (convMessageCursor != null) {
    969                 convMessageCursor.close();
    970             }
    971         }
    972         if (convLineInfos.isEmpty()) {
    973             return null;
    974         } else {
    975             return new ConversationInfoList(messageCount,
    976                     Lists.newLinkedList(convLineInfos.values()));
    977         }
    978     }
    979 
    980     /**
    981      * Scans all the attachments for a message and returns the most interesting one that we'll
    982      * show in a notification. By order of importance, in case there are multiple attachments:
    983      *      1- an image (because we can show the image as a BigPictureNotification)
    984      *      2- a video (because we can show a video frame as a BigPictureNotification)
    985      *      3- a vcard
    986      *      4- an audio attachment
    987      * @return MessagePartData for the most interesting part. Can be null.
    988      */
    989     private static MessagePartData getMostInterestingAttachment(
    990             final ConversationMessageData convMessageData) {
    991         final List<MessagePartData> attachments = convMessageData.getAttachments();
    992 
    993         MessagePartData imagePart = null;
    994         MessagePartData audioPart = null;
    995         MessagePartData vcardPart = null;
    996         MessagePartData videoPart = null;
    997 
    998         // 99.99% of the time there will be 0 or 1 part, since receiving slideshows is so
    999         // uncommon.
   1000 
   1001         // Remember the first of each type of part.
   1002         for (final MessagePartData messagePartData : attachments) {
   1003             if (messagePartData.isImage() && imagePart == null) {
   1004                 imagePart = messagePartData;
   1005             }
   1006             if (messagePartData.isVideo() && videoPart == null) {
   1007                 videoPart = messagePartData;
   1008             }
   1009             if (messagePartData.isVCard() && vcardPart == null) {
   1010                 vcardPart = messagePartData;
   1011             }
   1012             if (messagePartData.isAudio() && audioPart == null) {
   1013                 audioPart = messagePartData;
   1014             }
   1015         }
   1016         if (imagePart != null) {
   1017             return imagePart;
   1018         } else if (videoPart != null) {
   1019             return videoPart;
   1020         } else if (audioPart != null) {
   1021             return audioPart;
   1022         } else if (vcardPart != null) {
   1023             return vcardPart;
   1024         }
   1025         return null;
   1026     }
   1027 
   1028     private static int getMaxMessagesInConversationNotification() {
   1029         if (!BugleNotifications.isWearCompanionAppInstalled()) {
   1030             return BugleGservices.get().getInt(
   1031                     BugleGservicesKeys.MAX_MESSAGES_IN_CONVERSATION_NOTIFICATION,
   1032                     BugleGservicesKeys.MAX_MESSAGES_IN_CONVERSATION_NOTIFICATION_DEFAULT);
   1033         }
   1034         return BugleGservices.get().getInt(
   1035                 BugleGservicesKeys.MAX_MESSAGES_IN_CONVERSATION_NOTIFICATION_WITH_WEARABLE,
   1036                 BugleGservicesKeys.MAX_MESSAGES_IN_CONVERSATION_NOTIFICATION_WITH_WEARABLE_DEFAULT);
   1037     }
   1038 
   1039     /**
   1040      * Scans the database for messages that need to go into notifications. Creates the appropriate
   1041      * MessageNotificationState depending on if there are multiple senders, or
   1042      * messages from one sender.
   1043      * @return NotificationState for the notification created.
   1044      */
   1045     public static NotificationState getNotificationState() {
   1046         MessageNotificationState state = null;
   1047         final ConversationInfoList convList = createConversationInfoList();
   1048 
   1049         if (convList == null || convList.mConvInfos.size() == 0) {
   1050             if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
   1051                 LogUtil.v(TAG, "MessageNotificationState: No unseen notifications");
   1052             }
   1053         } else {
   1054             final ConversationLineInfo convInfo = convList.mConvInfos.get(0);
   1055             state = new MultiMessageNotificationState(convList);
   1056 
   1057             if (convList.mConvInfos.size() > 1) {
   1058                 // We've got notifications across multiple conversations. Pass in the notification
   1059                 // we just built of the most recent notification so we can use that to show the
   1060                 // user the new message in the ticker.
   1061                 state = new MultiConversationNotificationState(convList, state);
   1062             } else {
   1063                 // For now, only show avatars for notifications for a single conversation.
   1064                 if (convInfo.mAvatarUri != null) {
   1065                     if (state.mParticipantAvatarsUris == null) {
   1066                         state.mParticipantAvatarsUris = new ArrayList<Uri>(1);
   1067                     }
   1068                     state.mParticipantAvatarsUris.add(convInfo.mAvatarUri);
   1069                 }
   1070                 if (convInfo.mContactUri != null) {
   1071                     if (state.mParticipantContactUris == null) {
   1072                         state.mParticipantContactUris = new ArrayList<Uri>(1);
   1073                     }
   1074                     state.mParticipantContactUris.add(convInfo.mContactUri);
   1075                 }
   1076             }
   1077         }
   1078         if (state != null && LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
   1079             LogUtil.v(TAG, "MessageNotificationState: Notification state created"
   1080                     + ", title = " + LogUtil.sanitizePII(state.mTitle)
   1081                     + ", content = " + LogUtil.sanitizePII(state.mContent.toString()));
   1082         }
   1083         return state;
   1084     }
   1085 
   1086     protected String getTitle() {
   1087         return mTitle;
   1088     }
   1089 
   1090     @Override
   1091     public int getLatestMessageNotificationType() {
   1092         // This function is called to determine whether the most recent notification applies
   1093         // to an sms conversation or a hangout conversation. We have different ringtone/vibrate
   1094         // settings for both types of conversations.
   1095         if (mConvList.mConvInfos.size() > 0) {
   1096             final ConversationLineInfo convInfo = mConvList.mConvInfos.get(0);
   1097             return convInfo.getLatestMessageNotificationType();
   1098         }
   1099         return BugleNotifications.LOCAL_SMS_NOTIFICATION;
   1100     }
   1101 
   1102     @Override
   1103     public String getRingtoneUri() {
   1104         if (mConvList.mConvInfos.size() > 0) {
   1105             return mConvList.mConvInfos.get(0).mRingtoneUri;
   1106         }
   1107         return null;
   1108     }
   1109 
   1110     @Override
   1111     public boolean getNotificationVibrate() {
   1112         if (mConvList.mConvInfos.size() > 0) {
   1113             return mConvList.mConvInfos.get(0).mNotificationVibrate;
   1114         }
   1115         return false;
   1116     }
   1117 
   1118     protected CharSequence getTicker() {
   1119         return BugleNotifications.buildColonSeparatedMessage(
   1120                 mTickerSender != null ? mTickerSender : mTitle,
   1121                 mTickerText != null ? mTickerText : (mTickerNoContent ? null : mContent), null,
   1122                         null);
   1123     }
   1124 
   1125     private static CharSequence convertHtmlAndStripUrls(final String s) {
   1126         final Spanned text = Html.fromHtml(s);
   1127         if (text instanceof Spannable) {
   1128             stripUrls((Spannable) text);
   1129         }
   1130         return text;
   1131     }
   1132 
   1133     // Since we don't want to show URLs in notifications, a function
   1134     // to remove them in place.
   1135     private static void stripUrls(final Spannable text) {
   1136         final URLSpan[] spans = text.getSpans(0, text.length(), URLSpan.class);
   1137         for (final URLSpan span : spans) {
   1138             text.removeSpan(span);
   1139         }
   1140     }
   1141 
   1142     /*
   1143     private static void updateAlertStatusMessages(final long thresholdDeltaMs) {
   1144         // TODO may need this when supporting error notifications
   1145         final EsDatabaseHelper helper = EsDatabaseHelper.getDatabaseHelper();
   1146         final ContentValues values = new ContentValues();
   1147         final long nowMicros = System.currentTimeMillis() * 1000;
   1148         values.put(MessageColumns.ALERT_STATUS, "1");
   1149         final String selection =
   1150                 MessageColumns.ALERT_STATUS + "=0 AND (" +
   1151                 MessageColumns.STATUS + "=" + EsProvider.MESSAGE_STATUS_FAILED_TO_SEND + " OR (" +
   1152                 MessageColumns.STATUS + "!=" + EsProvider.MESSAGE_STATUS_ON_SERVER + " AND " +
   1153                 MessageColumns.TIMESTAMP + "+" + thresholdDeltaMs*1000 + "<" + nowMicros + ")) ";
   1154 
   1155         final int updateCount = helper.getWritableDatabaseWrapper().update(
   1156                 EsProvider.MESSAGES_TABLE,
   1157                 values,
   1158                 selection,
   1159                 null);
   1160         if (updateCount > 0) {
   1161             EsConversationsData.notifyConversationsChanged();
   1162         }
   1163     }*/
   1164 
   1165     static CharSequence applyWarningTextColor(final Context context,
   1166             final CharSequence text) {
   1167         if (text == null) {
   1168             return null;
   1169         }
   1170         final SpannableStringBuilder spanBuilder = new SpannableStringBuilder();
   1171         spanBuilder.append(text);
   1172         spanBuilder.setSpan(new ForegroundColorSpan(context.getResources().getColor(
   1173                 R.color.notification_warning_color)), 0, text.length(),
   1174                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
   1175         return spanBuilder;
   1176     }
   1177 
   1178     /**
   1179      * Check for failed messages and post notifications as needed.
   1180      * TODO: Rewrite this as a NotificationState.
   1181      */
   1182     public static void checkFailedMessages() {
   1183         final DatabaseWrapper db = DataModel.get().getDatabase();
   1184 
   1185         final Cursor messageDataCursor = db.query(DatabaseHelper.MESSAGES_TABLE,
   1186             MessageData.getProjection(),
   1187             FailedMessageQuery.FAILED_MESSAGES_WHERE_CLAUSE,
   1188             null /*selectionArgs*/,
   1189             null /*groupBy*/,
   1190             null /*having*/,
   1191             FailedMessageQuery.FAILED_ORDER_BY);
   1192 
   1193         try {
   1194             final Context context = Factory.get().getApplicationContext();
   1195             final Resources resources = context.getResources();
   1196             final NotificationManagerCompat notificationManager =
   1197                     NotificationManagerCompat.from(context);
   1198             if (messageDataCursor != null) {
   1199                 final MessageData messageData = new MessageData();
   1200 
   1201                 final HashSet<String> conversationsWithFailedMessages = new HashSet<String>();
   1202 
   1203                 // track row ids in case we want to display something that requires this
   1204                 // information
   1205                 final ArrayList<Integer> failedMessages = new ArrayList<Integer>();
   1206 
   1207                 int cursorPosition = -1;
   1208                 final long when = 0;
   1209 
   1210                 messageDataCursor.moveToPosition(-1);
   1211                 while (messageDataCursor.moveToNext()) {
   1212                     messageData.bind(messageDataCursor);
   1213 
   1214                     final String conversationId = messageData.getConversationId();
   1215                     if (DataModel.get().isNewMessageObservable(conversationId)) {
   1216                         // Don't post a system notification for an observable conversation
   1217                         // because we already show an angry red annotation in the conversation
   1218                         // itself or in the conversation preview snippet.
   1219                         continue;
   1220                     }
   1221 
   1222                     cursorPosition = messageDataCursor.getPosition();
   1223                     failedMessages.add(cursorPosition);
   1224                     conversationsWithFailedMessages.add(conversationId);
   1225                 }
   1226 
   1227                 if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
   1228                     LogUtil.d(TAG, "Found " + failedMessages.size() + " failed messages");
   1229                 }
   1230                 if (failedMessages.size() > 0) {
   1231                     final NotificationCompat.Builder builder =
   1232                             new NotificationCompat.Builder(context);
   1233 
   1234                     CharSequence line1;
   1235                     CharSequence line2;
   1236                     final boolean isRichContent = false;
   1237                     ConversationIdSet conversationIds = null;
   1238                     PendingIntent destinationIntent;
   1239                     if (failedMessages.size() == 1) {
   1240                         messageDataCursor.moveToPosition(cursorPosition);
   1241                         messageData.bind(messageDataCursor);
   1242                         final String conversationId =  messageData.getConversationId();
   1243 
   1244                         // We have a single conversation, go directly to that conversation.
   1245                         destinationIntent = UIIntents.get()
   1246                                 .getPendingIntentForConversationActivity(context,
   1247                                         conversationId,
   1248                                         null /*draft*/);
   1249 
   1250                         conversationIds = ConversationIdSet.createSet(conversationId);
   1251 
   1252                         final String failedMessgeSnippet = messageData.getMessageText();
   1253                         int failureStringId;
   1254                         if (messageData.getStatus() ==
   1255                                 MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED) {
   1256                             failureStringId =
   1257                                     R.string.notification_download_failures_line1_singular;
   1258                         } else {
   1259                             failureStringId = R.string.notification_send_failures_line1_singular;
   1260                         }
   1261                         line1 = resources.getString(failureStringId);
   1262                         line2 = failedMessgeSnippet;
   1263                         // Set rich text for non-SMS messages or MMS push notification messages
   1264                         // which we generate locally with rich text
   1265                         // TODO- fix this
   1266 //                        if (messageData.isMmsInd()) {
   1267 //                            isRichContent = true;
   1268 //                        }
   1269                     } else {
   1270                         // We have notifications for multiple conversation, go to the conversation
   1271                         // list.
   1272                         destinationIntent = UIIntents.get()
   1273                             .getPendingIntentForConversationListActivity(context);
   1274 
   1275                         int line1StringId;
   1276                         int line2PluralsId;
   1277                         if (messageData.getStatus() ==
   1278                                 MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED) {
   1279                             line1StringId =
   1280                                     R.string.notification_download_failures_line1_plural;
   1281                             line2PluralsId = R.plurals.notification_download_failures;
   1282                         } else {
   1283                             line1StringId = R.string.notification_send_failures_line1_plural;
   1284                             line2PluralsId = R.plurals.notification_send_failures;
   1285                         }
   1286                         line1 = resources.getString(line1StringId);
   1287                         line2 = resources.getQuantityString(
   1288                                 line2PluralsId,
   1289                                 conversationsWithFailedMessages.size(),
   1290                                 failedMessages.size(),
   1291                                 conversationsWithFailedMessages.size());
   1292                     }
   1293                     line1 = applyWarningTextColor(context, line1);
   1294                     line2 = applyWarningTextColor(context, line2);
   1295 
   1296                     final PendingIntent pendingIntentForDelete =
   1297                             UIIntents.get().getPendingIntentForClearingNotifications(
   1298                                     context,
   1299                                     BugleNotifications.UPDATE_ERRORS,
   1300                                     conversationIds,
   1301                                     0);
   1302 
   1303                     builder
   1304                         .setContentTitle(line1)
   1305                         .setTicker(line1)
   1306                         .setWhen(when > 0 ? when : System.currentTimeMillis())
   1307                         .setSmallIcon(R.drawable.ic_failed_light)
   1308                         .setDeleteIntent(pendingIntentForDelete)
   1309                         .setContentIntent(destinationIntent)
   1310                         .setSound(UriUtil.getUriForResourceId(context, R.raw.message_failure));
   1311                     if (isRichContent && !TextUtils.isEmpty(line2)) {
   1312                         final NotificationCompat.InboxStyle inboxStyle =
   1313                                 new NotificationCompat.InboxStyle(builder);
   1314                         if (line2 != null) {
   1315                             inboxStyle.addLine(Html.fromHtml(line2.toString()));
   1316                         }
   1317                         builder.setStyle(inboxStyle);
   1318                     } else {
   1319                         builder.setContentText(line2);
   1320                     }
   1321 
   1322                     if (builder != null) {
   1323                         notificationManager.notify(
   1324                                 BugleNotifications.buildNotificationTag(
   1325                                         PendingIntentConstants.MSG_SEND_ERROR, null),
   1326                                 PendingIntentConstants.MSG_SEND_ERROR,
   1327                                 builder.build());
   1328                     }
   1329                 } else {
   1330                     notificationManager.cancel(
   1331                             BugleNotifications.buildNotificationTag(
   1332                                     PendingIntentConstants.MSG_SEND_ERROR, null),
   1333                             PendingIntentConstants.MSG_SEND_ERROR);
   1334                 }
   1335             }
   1336         } finally {
   1337             if (messageDataCursor != null) {
   1338                 messageDataCursor.close();
   1339             }
   1340         }
   1341     }
   1342 }
   1343