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 */
16package com.android.messaging.datamodel;
17
18import android.app.Notification;
19import android.app.PendingIntent;
20import android.content.Context;
21import android.content.res.Resources;
22import android.database.Cursor;
23import android.graphics.Typeface;
24import android.net.Uri;
25import android.support.v4.app.NotificationCompat;
26import android.support.v4.app.NotificationCompat.Builder;
27import android.support.v4.app.NotificationCompat.WearableExtender;
28import android.support.v4.app.NotificationManagerCompat;
29import android.text.Html;
30import android.text.Spannable;
31import android.text.SpannableString;
32import android.text.SpannableStringBuilder;
33import android.text.Spanned;
34import android.text.TextUtils;
35import android.text.style.ForegroundColorSpan;
36import android.text.style.StyleSpan;
37import android.text.style.TextAppearanceSpan;
38import android.text.style.URLSpan;
39
40import com.android.messaging.Factory;
41import com.android.messaging.R;
42import com.android.messaging.datamodel.data.ConversationListItemData;
43import com.android.messaging.datamodel.data.ConversationMessageData;
44import com.android.messaging.datamodel.data.ConversationParticipantsData;
45import com.android.messaging.datamodel.data.MessageData;
46import com.android.messaging.datamodel.data.MessagePartData;
47import com.android.messaging.datamodel.data.ParticipantData;
48import com.android.messaging.datamodel.media.VideoThumbnailRequest;
49import com.android.messaging.sms.MmsUtils;
50import com.android.messaging.ui.UIIntents;
51import com.android.messaging.util.Assert;
52import com.android.messaging.util.AvatarUriUtil;
53import com.android.messaging.util.BugleGservices;
54import com.android.messaging.util.BugleGservicesKeys;
55import com.android.messaging.util.ContentType;
56import com.android.messaging.util.ConversationIdSet;
57import com.android.messaging.util.LogUtil;
58import com.android.messaging.util.PendingIntentConstants;
59import com.android.messaging.util.UriUtil;
60import com.google.common.collect.Lists;
61
62import java.util.ArrayList;
63import java.util.HashMap;
64import java.util.HashSet;
65import java.util.Iterator;
66import java.util.LinkedHashMap;
67import java.util.List;
68import 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 */
88public 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