summaryrefslogtreecommitdiffstats
path: root/src/com/android/messaging/datamodel/MessageNotificationState.java
blob: a7af329105dcc75ae69d79ffc290ea2f607a1df1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.messaging.datamodel;

import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Typeface;
import android.net.Uri;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationCompat.Builder;
import androidx.core.app.NotificationCompat.WearableExtender;
import androidx.core.app.NotificationManagerCompat;
import android.text.Html;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.text.style.TextAppearanceSpan;
import android.text.style.URLSpan;

import com.android.messaging.Factory;
import com.android.messaging.R;
import com.android.messaging.datamodel.data.ConversationListItemData;
import com.android.messaging.datamodel.data.ConversationMessageData;
import com.android.messaging.datamodel.data.ConversationParticipantsData;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.datamodel.data.MessagePartData;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.datamodel.media.VideoThumbnailRequest;
import com.android.messaging.sms.MmsUtils;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.util.Assert;
import com.android.messaging.util.AvatarUriUtil;
import com.android.messaging.util.BugleGservices;
import com.android.messaging.util.BugleGservicesKeys;
import com.android.messaging.util.ContentType;
import com.android.messaging.util.ConversationIdSet;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.PendingIntentConstants;
import com.android.messaging.util.UriUtil;
import com.google.common.collect.Lists;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Notification building class for conversation messages.
 *
 * Message Notifications are built in several stages with several utility classes.
 * 1) Perform a database query and fill a data structure with information on messages and
 *    conversations which need to be notified.
 * 2) Based on the data structure choose an appropriate NotificationState subclass to
 *    represent all the notifications.
 *    -- For one or more messages in one conversation: MultiMessageNotificationState.
 *    -- For multiple messages in multiple conversations: MultiConversationNotificationState
 *
 *  A three level structure is used to coalesce the data from the database. From bottom to top:
 *  1) NotificationLineInfo - A single message that needs to be notified.
 *  2) ConversationLineInfo - A list of NotificationLineInfo in a single conversation.
 *  3) ConversationInfoList - A list of ConversationLineInfo and the total number of messages.
 *
 *  The createConversationInfoList function performs the query and creates the data structure.
 */
public abstract class MessageNotificationState extends NotificationState {
    // Logging
    static final String TAG = LogUtil.BUGLE_NOTIFICATIONS_TAG;
    private static final int MAX_MESSAGES_IN_WEARABLE_PAGE = 20;

    private static final int MAX_CHARACTERS_IN_GROUP_NAME = 30;

    private static final int REPLY_INTENT_REQUEST_CODE_OFFSET = 0;
    private static final int NUM_EXTRA_REQUEST_CODES_NEEDED = 1;
    protected String mTickerSender = null;
    protected CharSequence mTickerText = null;
    protected String mTitle = null;
    protected CharSequence mContent = null;
    protected Uri mAttachmentUri = null;
    protected String mAttachmentType = null;
    protected boolean mTickerNoContent;

    @Override
    protected Uri getAttachmentUri() {
        return mAttachmentUri;
    }

    @Override
    protected String getAttachmentType() {
        return mAttachmentType;
    }

    @Override
    public int getIcon() {
        return R.drawable.ic_sms_light;
    }

    @Override
    public int getPriority() {
        // Returning PRIORITY_HIGH causes L to put up a HUD notification. Without it, the ticker
        // isn't displayed.
        return Notification.PRIORITY_HIGH;
    }

    /**
     * Base class for single notification events for messages. Multiple of these
     * may be grouped into a single conversation.
     */
    static class NotificationLineInfo {

        final int mNotificationType;

        NotificationLineInfo() {
            mNotificationType = BugleNotifications.LOCAL_SMS_NOTIFICATION;
        }

        NotificationLineInfo(final int notificationType) {
            mNotificationType = notificationType;
        }
    }

    /**
     * Information on a single chat message which should be shown in a notification.
     */
    static class MessageLineInfo extends NotificationLineInfo {
        final CharSequence mText;
        Uri mAttachmentUri;
        String mAttachmentType;
        final String mAuthorFullName;
        final String mAuthorFirstName;
        boolean mIsManualDownloadNeeded;
        final String mMessageId;

        MessageLineInfo(final boolean isGroup, final String authorFullName,
                final String authorFirstName, final CharSequence text, final Uri attachmentUrl,
                final String attachmentType, final boolean isManualDownloadNeeded,
                final String messageId) {
            super(BugleNotifications.LOCAL_SMS_NOTIFICATION);
            mAuthorFullName = authorFullName;
            mAuthorFirstName = authorFirstName;
            mText = text;
            mAttachmentUri = attachmentUrl;
            mAttachmentType = attachmentType;
            mIsManualDownloadNeeded = isManualDownloadNeeded;
            mMessageId = messageId;
        }
    }

    /**
     * Information on all the notification messages within a single conversation.
     */
    static class ConversationLineInfo {
        // Conversation id of the latest message in the notification for this merged conversation.
        final String mConversationId;

        // True if this represents a group conversation.
        final boolean mIsGroup;

        // Name of the group conversation if available.
        final String mGroupConversationName;

        // True if this conversation's recipients includes one or more email address(es)
        // (see ConversationColumns.INCLUDE_EMAIL_ADDRESS)
        final boolean mIncludeEmailAddress;

        // Timestamp of the latest message
        final long mReceivedTimestamp;

        // Self participant id.
        final String mSelfParticipantId;

        // List of individual line notifications to be parsed later.
        final List<NotificationLineInfo> mLineInfos;

        // Total number of messages. Might be different that mLineInfos.size() as the number of
        // line infos is capped.
        int mTotalMessageCount;

        // Custom ringtone if set
        final String mRingtoneUri;

        // Should notification be enabled for this conversation?
        final boolean mNotificationEnabled;

        // Should notifications vibrate for this conversation?
        final boolean mNotificationVibrate;

        // Avatar uri of sender
        final Uri mAvatarUri;

        // Contact uri of sender
        final Uri mContactUri;

        // Subscription id.
        final int mSubId;

        // Number of participants
        final int mParticipantCount;

        public ConversationLineInfo(final String conversationId,
                final boolean isGroup,
                final String groupConversationName,
                final boolean includeEmailAddress,
                final long receivedTimestamp,
                final String selfParticipantId,
                final String ringtoneUri,
                final boolean notificationEnabled,
                final boolean notificationVibrate,
                final Uri avatarUri,
                final Uri contactUri,
                final int subId,
                final int participantCount) {
            mConversationId = conversationId;
            mIsGroup = isGroup;
            mGroupConversationName = groupConversationName;
            mIncludeEmailAddress = includeEmailAddress;
            mReceivedTimestamp = receivedTimestamp;
            mSelfParticipantId = selfParticipantId;
            mLineInfos = new ArrayList<NotificationLineInfo>();
            mTotalMessageCount = 0;
            mRingtoneUri = ringtoneUri;
            mAvatarUri = avatarUri;
            mContactUri = contactUri;
            mNotificationEnabled = notificationEnabled;
            mNotificationVibrate = notificationVibrate;
            mSubId = subId;
            mParticipantCount = participantCount;
        }

        public int getLatestMessageNotificationType() {
            final MessageLineInfo messageLineInfo = getLatestMessageLineInfo();
            if (messageLineInfo == null) {
                return BugleNotifications.LOCAL_SMS_NOTIFICATION;
            }
            return messageLineInfo.mNotificationType;
        }

        public String getLatestMessageId() {
            final MessageLineInfo messageLineInfo = getLatestMessageLineInfo();
            if (messageLineInfo == null) {
                return null;
            }
            return messageLineInfo.mMessageId;
        }

        public boolean getDoesLatestMessageNeedDownload() {
            final MessageLineInfo messageLineInfo = getLatestMessageLineInfo();
            if (messageLineInfo == null) {
                return false;
            }
            return messageLineInfo.mIsManualDownloadNeeded;
        }

        private MessageLineInfo getLatestMessageLineInfo() {
            // The latest message is stored at index zero of the message line infos.
            if (mLineInfos.size() > 0 && mLineInfos.get(0) instanceof MessageLineInfo) {
                return (MessageLineInfo) mLineInfos.get(0);
            }
            return null;
        }
    }

    /**
     * Information on all the notification messages across all conversations.
     */
    public static class ConversationInfoList {
        final int mMessageCount;
        final List<ConversationLineInfo> mConvInfos;
        public ConversationInfoList(final int count, final List<ConversationLineInfo> infos) {
            mMessageCount = count;
            mConvInfos = infos;
        }
    }

    final ConversationInfoList mConvList;
    private long mLatestReceivedTimestamp;

    private static ConversationIdSet makeConversationIdSet(final ConversationInfoList convList) {
        ConversationIdSet set = null;
        if (convList != null && convList.mConvInfos != null && convList.mConvInfos.size() > 0) {
            set = new ConversationIdSet();
            for (final ConversationLineInfo info : convList.mConvInfos) {
                    set.add(info.mConversationId);
            }
        }
        return set;
    }

    protected MessageNotificationState(final ConversationInfoList convList) {
        super(makeConversationIdSet(convList));
        mConvList = convList;
        mType = PendingIntentConstants.SMS_NOTIFICATION_ID;
        mLatestReceivedTimestamp = Long.MIN_VALUE;
        if (convList != null) {
            for (final ConversationLineInfo info : convList.mConvInfos) {
                mLatestReceivedTimestamp = Math.max(mLatestReceivedTimestamp,
                        info.mReceivedTimestamp);
            }
        }
    }

    @Override
    public long getLatestReceivedTimestamp() {
        return mLatestReceivedTimestamp;
    }

    @Override
    public int getNumRequestCodesNeeded() {
        // Get additional request codes for the Reply PendingIntent (wearables only)
        // and the DND PendingIntent.
        return super.getNumRequestCodesNeeded() + NUM_EXTRA_REQUEST_CODES_NEEDED;
    }

    private int getBaseExtraRequestCode() {
        return mBaseRequestCode + super.getNumRequestCodesNeeded();
    }

    public int getReplyIntentRequestCode() {
        return getBaseExtraRequestCode() + REPLY_INTENT_REQUEST_CODE_OFFSET;
    }

    @Override
    public PendingIntent getClearIntent() {
        return UIIntents.get().getPendingIntentForClearingNotifications(
                    Factory.get().getApplicationContext(),
                    BugleNotifications.UPDATE_MESSAGES,
                    mConversationIds,
                    getClearIntentRequestCode());
    }

    @Override
    public PendingIntent getReadIntent() {
        return UIIntents.get().getPendingIntentForMarkingAsRead(
                    Factory.get().getApplicationContext(),
                    mConversationIds,
                    getReadIntentRequestCode());
    }

    /**
     * Notification for multiple messages in at least 2 different conversations.
     */
    public static class MultiConversationNotificationState extends MessageNotificationState {

        public final List<MessageNotificationState>
                mChildren = new ArrayList<MessageNotificationState>();

        public MultiConversationNotificationState(
                final ConversationInfoList convList, final MessageNotificationState state) {
            super(convList);
            mAttachmentUri = null;
            mAttachmentType = null;

            // Pull the ticker title/text from the single notification
            mTickerSender = state.getTitle();
            mTitle = Factory.get().getApplicationContext().getResources().getQuantityString(
                    R.plurals.notification_new_messages,
                    convList.mMessageCount, convList.mMessageCount);
            mTickerText = state.mContent;

            // Create child notifications for each conversation,
            // which will be displayed (only) on a wearable device.
            for (int i = 0; i < convList.mConvInfos.size(); i++) {
                final ConversationLineInfo convInfo = convList.mConvInfos.get(i);
                if (!(convInfo.mLineInfos.get(0) instanceof MessageLineInfo)) {
                    continue;
                }
                setPeopleForConversation(convInfo.mConversationId);
                final ConversationInfoList list = new ConversationInfoList(
                        convInfo.mTotalMessageCount, Lists.newArrayList(convInfo));
                mChildren.add(new BundledMessageNotificationState(list, i));
            }
        }

        @Override
        public int getIcon() {
            return R.drawable.ic_sms_multi_light;
        }

        @Override
        protected NotificationCompat.Style build(final Builder builder) {
            builder.setContentTitle(mTitle);
            NotificationCompat.InboxStyle inboxStyle = null;
            inboxStyle = new NotificationCompat.InboxStyle(builder);

            final Context context = Factory.get().getApplicationContext();
            // enumeration_comma is defined as ", "
            final String separator = context.getString(R.string.enumeration_comma);
            final StringBuilder senders = new StringBuilder();
            long when = 0;
            for (int i = 0; i < mConvList.mConvInfos.size(); i++) {
                final ConversationLineInfo convInfo = mConvList.mConvInfos.get(i);
                if (convInfo.mReceivedTimestamp > when) {
                    when = convInfo.mReceivedTimestamp;
                }
                String sender;
                CharSequence text;
                final NotificationLineInfo lineInfo = convInfo.mLineInfos.get(0);
                final MessageLineInfo messageLineInfo = (MessageLineInfo) lineInfo;
                if (convInfo.mIsGroup) {
                    sender = (convInfo.mGroupConversationName.length() >
                            MAX_CHARACTERS_IN_GROUP_NAME) ?
                                    truncateGroupMessageName(convInfo.mGroupConversationName)
                                    : convInfo.mGroupConversationName;
                } else {
                    sender = messageLineInfo.mAuthorFullName;
                }
                text = messageLineInfo.mText;
                mAttachmentUri = messageLineInfo.mAttachmentUri;
                mAttachmentType = messageLineInfo.mAttachmentType;

                inboxStyle.addLine(BugleNotifications.formatInboxMessage(
                        sender, text, mAttachmentUri, mAttachmentType));
                if (sender != null) {
                    if (senders.length() > 0) {
                        senders.append(separator);
                    }
                    senders.append(sender);
                }
            }
            // for collapsed state
            mContent = senders;
            builder.setContentText(senders)
                .setTicker(getTicker())
                .setWhen(when);

            return inboxStyle;
        }
    }

    /**
     * Truncate group conversation name to be displayed in the notifications. This either truncates
     * the entire group name or finds the last comma in the available length and truncates the name
     * at that point
     */
    private static String truncateGroupMessageName(final String conversationName) {
        int endIndex = MAX_CHARACTERS_IN_GROUP_NAME;
        for (int i = MAX_CHARACTERS_IN_GROUP_NAME; i >= 0; i--) {
            // The dividing marker should stay consistent with ConversationListItemData.DIVIDER_TEXT
            if (conversationName.charAt(i) == ',') {
                endIndex = i;
                break;
            }
        }
        return conversationName.substring(0, endIndex) + '\u2026';
    }

    /**
     * Notification for multiple messages in a single conversation. Also used if there is a single
     * message in a single conversation.
     */
    public static class MultiMessageNotificationState extends MessageNotificationState {

        public MultiMessageNotificationState(final ConversationInfoList convList) {
            super(convList);
            // This conversation has been accepted.
            final ConversationLineInfo convInfo = convList.mConvInfos.get(0);
            setAvatarUrlsForConversation(convInfo.mConversationId);
            setPeopleForConversation(convInfo.mConversationId);

            final Context context = Factory.get().getApplicationContext();
            MessageLineInfo messageInfo = (MessageLineInfo) convInfo.mLineInfos.get(0);
            // attached photo
            mAttachmentUri = messageInfo.mAttachmentUri;
            mAttachmentType = messageInfo.mAttachmentType;
            mContent = messageInfo.mText;

            if (mAttachmentUri != null) {
                // The default attachment type is an image, since that's what was originally
                // supported. When there's no content type, assume it's an image.
                int message = R.string.notification_picture;
                if (ContentType.isAudioType(mAttachmentType)) {
                    message = R.string.notification_audio;
                } else if (ContentType.isVideoType(mAttachmentType)) {
                    message = R.string.notification_video;
                } else if (ContentType.isVCardType(mAttachmentType)) {
                    message = R.string.notification_vcard;
                }
                final String attachment = context.getString(message);
                final SpannableStringBuilder spanBuilder = new SpannableStringBuilder();
                if (!TextUtils.isEmpty(mContent)) {
                    spanBuilder.append(mContent).append(System.getProperty("line.separator"));
                }
                final int start = spanBuilder.length();
                spanBuilder.append(attachment);
                spanBuilder.setSpan(new StyleSpan(Typeface.ITALIC), start, spanBuilder.length(),
                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                mContent = spanBuilder;
            }
            if (convInfo.mIsGroup) {
                // When the message is part of a group, the sender's first name
                // is prepended to the message, but not for the ticker message.
                mTickerText = mContent;
                mTickerSender = messageInfo.mAuthorFullName;
                // append the bold name to the front of the message
                mContent = BugleNotifications.buildSpaceSeparatedMessage(
                        messageInfo.mAuthorFullName, mContent, mAttachmentUri,
                        mAttachmentType);
                mTitle = convInfo.mGroupConversationName;
            } else {
                // No matter how many messages there are, since this is a 1:1, just
                // get the author full name from the first one.
                messageInfo = (MessageLineInfo) convInfo.mLineInfos.get(0);
                mTitle = messageInfo.mAuthorFullName;
            }
        }

        @Override
        protected NotificationCompat.Style build(final Builder builder) {
            builder.setContentTitle(mTitle)
                .setTicker(getTicker());

            NotificationCompat.Style notifStyle = null;
            final ConversationLineInfo convInfo = mConvList.mConvInfos.get(0);
            final List<NotificationLineInfo> lineInfos = convInfo.mLineInfos;
            final int messageCount = lineInfos.size();
            // At this point, all the messages come from the same conversation. We need to load
            // the sender's avatar and then finish building the notification on a callback.

            builder.setContentText(mContent);   // for collapsed state

            if (messageCount == 1) {
                final boolean shouldShowImage = ContentType.isImageType(mAttachmentType)
                        || (ContentType.isVideoType(mAttachmentType)
                        && VideoThumbnailRequest.shouldShowIncomingVideoThumbnails());
                if (mAttachmentUri != null && shouldShowImage) {
                    // Show "Picture" as the content
                    final MessageLineInfo messageLineInfo = (MessageLineInfo) lineInfos.get(0);
                    String authorFirstName = messageLineInfo.mAuthorFirstName;

                    // For the collapsed state, just show "picture" unless this is a
                    // group conversation. If it's a group, show the sender name and
                    // "picture".
                    final CharSequence tickerTag =
                            BugleNotifications.formatAttachmentTag(authorFirstName,
                                    mAttachmentType);
                    // For 1:1 notifications don't show first name in the notification, but
                    // do show it in the ticker text
                    CharSequence pictureTag = tickerTag;
                    if (!convInfo.mIsGroup) {
                        authorFirstName = null;
                        pictureTag = BugleNotifications.formatAttachmentTag(authorFirstName,
                                mAttachmentType);
                    }
                    builder.setContentText(pictureTag);
                    builder.setTicker(tickerTag);

                    notifStyle = new NotificationCompat.BigPictureStyle(builder)
                        .setSummaryText(BugleNotifications.formatInboxMessage(
                                authorFirstName,
                                null, null,
                                null));  // expanded state, just show sender
                } else {
                    notifStyle = new NotificationCompat.BigTextStyle(builder)
                    .bigText(mContent);
                }
            } else {
                // We've got multiple messages for the same sender.
                // Starting with the oldest new message, display the full text of each message.
                // Begin a line for each subsequent message.
                final SpannableStringBuilder buf = new SpannableStringBuilder();

                for (int i = lineInfos.size() - 1; i >= 0; --i) {
                    final NotificationLineInfo info = lineInfos.get(i);
                    final MessageLineInfo messageLineInfo = (MessageLineInfo) info;
                    mAttachmentUri = messageLineInfo.mAttachmentUri;
                    mAttachmentType = messageLineInfo.mAttachmentType;
                    CharSequence text = messageLineInfo.mText;
                    if (!TextUtils.isEmpty(text) || mAttachmentUri != null) {
                        if (convInfo.mIsGroup) {
                            // append the bold name to the front of the message
                            text = BugleNotifications.buildSpaceSeparatedMessage(
                                    messageLineInfo.mAuthorFullName, text, mAttachmentUri,
                                    mAttachmentType);
                        } else {
                            text = BugleNotifications.buildSpaceSeparatedMessage(
                                    null, text, mAttachmentUri, mAttachmentType);
                        }
                        buf.append(text);
                        if (i > 0) {
                            buf.append('\n');
                        }
                    }
                }

                // Show a single notification -- big style with the text of all the messages
                notifStyle = new NotificationCompat.BigTextStyle(builder).bigText(buf);
            }
            builder.setWhen(convInfo.mReceivedTimestamp);
            return notifStyle;
        }

    }

    private static boolean firstNameUsedMoreThanOnce(
            final HashMap<String, Integer> map, final String firstName) {
        if (map == null) {
            return false;
        }
        if (firstName == null) {
            return false;
        }
        final Integer count = map.get(firstName);
        if (count != null) {
            return count > 1;
        } else {
            return false;
        }
    }

    private static HashMap<String, Integer> scanFirstNames(final String conversationId) {
        final Context context = Factory.get().getApplicationContext();
        final Uri uri =
                MessagingContentProvider.buildConversationParticipantsUri(conversationId);
        final Cursor participantsCursor = context.getContentResolver().query(
                uri, ParticipantData.ParticipantsQuery.PROJECTION, null, null, null);
        final ConversationParticipantsData participantsData = new ConversationParticipantsData();
        participantsData.bind(participantsCursor);
        final Iterator<ParticipantData> iter = participantsData.iterator();

        final HashMap<String, Integer> firstNames = new HashMap<String, Integer>();
        boolean seenSelf = false;
        while (iter.hasNext()) {
            final ParticipantData participant = iter.next();
            // Make sure we only add the self participant once
            if (participant.isSelf()) {
                if (seenSelf) {
                    continue;
                } else {
                    seenSelf = true;
                }
            }

            final String firstName = participant.getFirstName();
            if (firstName == null) {
                continue;
            }

            final int currentCount = firstNames.containsKey(firstName)
                    ? firstNames.get(firstName)
                    : 0;
            firstNames.put(firstName, currentCount + 1);
        }
        return firstNames;
    }

    // Essentially, we're building a list of the past 20 messages for this conversation to display
    // on the wearable.
    public static Notification buildConversationPageForWearable(final String conversationId,
            int participantCount) {
        final Context context = Factory.get().getApplicationContext();

        // Limit the number of messages to show. We just want enough to provide context for the
        // notification. Fetch one more than we need, so we can tell if there are more messages
        // before the one we're showing.
        // TODO: in the query, a multipart message will contain a row for each part.
        // We might need a smarter GROUP_BY. On the other hand, we might want to show each of the
        // parts as separate messages on the wearable.
        final int limit = MAX_MESSAGES_IN_WEARABLE_PAGE + 1;

        final List<CharSequence> messages = Lists.newArrayList();
        boolean hasSeenMessagesBeforeNotification = false;
        Cursor convMessageCursor = null;
        try {
            final DatabaseWrapper db = DataModel.get().getDatabase();

            final String[] queryArgs = { conversationId };
            final String convPageSql = ConversationMessageData.getWearableQuerySql() + " LIMIT " +
                    limit;
            convMessageCursor = db.rawQuery(
                    convPageSql,
                    queryArgs);

            if (convMessageCursor == null || !convMessageCursor.moveToFirst()) {
                return null;
            }
            final ConversationMessageData convMessageData =
                    new ConversationMessageData();

            final HashMap<String, Integer> firstNames = scanFirstNames(conversationId);
            do {
                convMessageData.bind(convMessageCursor);

                final String authorFullName = convMessageData.getSenderFullName();
                final String authorFirstName = convMessageData.getSenderFirstName();
                String text = convMessageData.getText();

                final boolean isSmsPushNotification = convMessageData.getIsMmsNotification();

                // if auto-download was off to show a message to tap to download the message. We
                // might need to get that working again.
                if (isSmsPushNotification && text != null) {
                    text = convertHtmlAndStripUrls(text).toString();
                }
                // Skip messages without any content
                if (TextUtils.isEmpty(text) && !convMessageData.hasAttachments()) {
                    continue;
                }
                // Track whether there are messages prior to the one(s) shown in the notification.
                if (convMessageData.getIsSeen()) {
                    hasSeenMessagesBeforeNotification = true;
                }

                final boolean usedMoreThanOnce = firstNameUsedMoreThanOnce(
                        firstNames, authorFirstName);
                String displayName = usedMoreThanOnce ? authorFullName : authorFirstName;
                if (TextUtils.isEmpty(displayName)) {
                    if (convMessageData.getIsIncoming()) {
                        displayName = convMessageData.getSenderDisplayDestination();
                        if (TextUtils.isEmpty(displayName)) {
                            displayName = context.getString(R.string.unknown_sender);
                        }
                    } else {
                        displayName = context.getString(R.string.unknown_self_participant);
                    }
                }

                Uri attachmentUri = null;
                String attachmentType = null;
                final List<MessagePartData> attachments = convMessageData.getAttachments();
                for (final MessagePartData messagePartData : attachments) {
                    // Look for the first attachment that's not the text piece.
                    if (!messagePartData.isText()) {
                        attachmentUri = messagePartData.getContentUri();
                        attachmentType = messagePartData.getContentType();
                        break;
                    }
                }

                final CharSequence message = BugleNotifications.buildSpaceSeparatedMessage(
                        displayName, text, attachmentUri, attachmentType);
                messages.add(message);

            } while (convMessageCursor.moveToNext());
        } finally {
            if (convMessageCursor != null) {
                convMessageCursor.close();
            }
        }

        // If there is no conversation history prior to what is already visible in the main
        // notification, there's no need to include the conversation log, too.
        final int maxMessagesInNotification = getMaxMessagesInConversationNotification();
        if (!hasSeenMessagesBeforeNotification && messages.size() <= maxMessagesInNotification) {
            return null;
        }

        final SpannableStringBuilder bigText = new SpannableStringBuilder();
        // There is at least 1 message prior to the first one that we're going to show.
        // Indicate this by inserting an ellipsis at the beginning of the conversation log.
        if (convMessageCursor.getCount() == limit) {
            bigText.append(context.getString(R.string.ellipsis) + "\n\n");
            if (messages.size() > MAX_MESSAGES_IN_WEARABLE_PAGE) {
                messages.remove(messages.size() - 1);
            }
        }
        // Messages are sorted in descending timestamp order, so iterate backwards
        // to get them back in ascending order for display purposes.
        for (int i = messages.size() - 1; i >= 0; --i) {
            bigText.append(messages.get(i));
            if (i > 0) {
                bigText.append("\n\n");
            }
        }
        ++participantCount;     // Add in myself

        if (participantCount > 2) {
            final SpannableString statusText = new SpannableString(
                    context.getResources().getQuantityString(R.plurals.wearable_participant_count,
                            participantCount, participantCount));
            statusText.setSpan(new ForegroundColorSpan(context.getResources().getColor(
                    R.color.wearable_notification_participants_count)), 0, statusText.length(),
                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            bigText.append("\n\n").append(statusText);
        }

        final NotificationCompat.Builder notifBuilder = new NotificationCompat.Builder(context);
        final NotificationCompat.Style notifStyle =
                new NotificationCompat.BigTextStyle(notifBuilder).bigText(bigText);
        notifBuilder.setStyle(notifStyle);

        final WearableExtender wearableExtender = new WearableExtender();
        wearableExtender.setStartScrollBottom(true);
        notifBuilder.extend(wearableExtender);

        return notifBuilder.build();
    }

    /**
     * Notification for one or more messages in a single conversation, which is bundled together
     * with notifications for other conversations on a wearable device.
     */
    public static class BundledMessageNotificationState extends MultiMessageNotificationState {
        public int mGroupOrder;
        public BundledMessageNotificationState(final ConversationInfoList convList,
                final int groupOrder) {
            super(convList);
            mGroupOrder = groupOrder;
        }
    }

    /**
     * Performs a query on the database.
     */
    private static ConversationInfoList createConversationInfoList() {
        // Map key is conversation id. We use LinkedHashMap to ensure that entries are iterated in
        // the same order they were originally added. We scan unseen messages from newest to oldest,
        // so the corresponding conversations are added in that order, too.
        final Map<String, ConversationLineInfo> convLineInfos = new LinkedHashMap<>();
        int messageCount = 0;

        Cursor convMessageCursor = null;
        try {
            final Context context = Factory.get().getApplicationContext();
            final DatabaseWrapper db = DataModel.get().getDatabase();

            convMessageCursor = db.rawQuery(
                    ConversationMessageData.getNotificationQuerySql(),
                    null);

            if (convMessageCursor != null && convMessageCursor.moveToFirst()) {
                if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
                    LogUtil.v(TAG, "MessageNotificationState: Found unseen message notifications.");
                }
                final ConversationMessageData convMessageData =
                        new ConversationMessageData();

                HashMap<String, Integer> firstNames = null;
                String conversationIdForFirstNames = null;
                String groupConversationName = null;
                final int maxMessages = getMaxMessagesInConversationNotification();

                do {
                    convMessageData.bind(convMessageCursor);

                    // First figure out if this is a valid message.
                    String authorFullName = convMessageData.getSenderFullName();
                    String authorFirstName = convMessageData.getSenderFirstName();
                    final String messageText = convMessageData.getText();

                    final String convId = convMessageData.getConversationId();
                    final String messageId = convMessageData.getMessageId();

                    CharSequence text = messageText;
                    final boolean isManualDownloadNeeded = convMessageData.getIsMmsNotification();
                    if (isManualDownloadNeeded) {
                        // Don't try and convert the text from html if it's sms and not a sms push
                        // notification.
                        Assert.equals(MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD,
                                convMessageData.getStatus());
                        text = context.getResources().getString(
                                R.string.message_title_manual_download);
                    }
                    ConversationLineInfo currConvInfo = convLineInfos.get(convId);
                    if (currConvInfo == null) {
                        final ConversationListItemData convData =
                                ConversationListItemData.getExistingConversation(db, convId);
                        if (!convData.getNotificationEnabled()) {
                            // Skip conversations that have notifications disabled.
                            continue;
                        }
                        final int subId = BugleDatabaseOperations.getSelfSubscriptionId(db,
                                convData.getSelfId());
                        groupConversationName = convData.getName();
                        final Uri avatarUri = AvatarUriUtil.createAvatarUri(
                                convMessageData.getSenderProfilePhotoUri(),
                                convMessageData.getSenderFullName(),
                                convMessageData.getSenderNormalizedDestination(),
                                convMessageData.getSenderContactLookupKey());
                        currConvInfo = new ConversationLineInfo(convId,
                                convData.getIsGroup(),
                                groupConversationName,
                                convData.getIncludeEmailAddress(),
                                convMessageData.getReceivedTimeStamp(),
                                convData.getSelfId(),
                                convData.getNotificationSoundUri(),
                                convData.getNotificationEnabled(),
                                convData.getNotifiationVibrate(),
                                avatarUri,
                                convMessageData.getSenderContactLookupUri(),
                                subId,
                                convData.getParticipantCount());
                        convLineInfos.put(convId, currConvInfo);
                    }
                    // Prepare the message line
                    if (currConvInfo.mTotalMessageCount < maxMessages) {
                        if (currConvInfo.mIsGroup) {
                            if (authorFirstName == null) {
                                // authorFullName might be null as well. In that case, we won't
                                // show an author. That is better than showing all the group
                                // names again on the 2nd line.
                                authorFirstName = authorFullName;
                            }
                        } else {
                            // don't recompute this if we don't need to
                            if (!TextUtils.equals(conversationIdForFirstNames, convId)) {
                                firstNames = scanFirstNames(convId);
                                conversationIdForFirstNames = convId;
                            }
                            if (firstNames != null) {
                                final Integer count = firstNames.get(authorFirstName);
                                if (count != null && count > 1) {
                                    authorFirstName = authorFullName;
                                }
                            }

                            if (authorFullName == null) {
                                authorFullName = groupConversationName;
                            }
                            if (authorFirstName == null) {
                                authorFirstName = groupConversationName;
                            }
                        }
                        final String subjectText = MmsUtils.cleanseMmsSubject(
                                context.getResources(),
                                convMessageData.getMmsSubject());
                        if (!TextUtils.isEmpty(subjectText)) {
                            final String subjectLabel =
                                    context.getString(R.string.subject_label);
                            final SpannableStringBuilder spanBuilder =
                                    new SpannableStringBuilder();

                            spanBuilder.append(context.getString(R.string.notification_subject,
                                    subjectLabel, subjectText));
                            spanBuilder.setSpan(new TextAppearanceSpan(
                                    context, R.style.NotificationSubjectText), 0,
                                    subjectLabel.length(),
                                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                            if (!TextUtils.isEmpty(text)) {
                                // Now add the actual message text below the subject header.
                                spanBuilder.append(System.getProperty("line.separator") + text);
                            }
                            text = spanBuilder;
                        }
                        // If we've got attachments, find the best one. If one of the messages is
                        // a photo, save the url so we'll display a big picture notification.
                        // Otherwise, show the first one we find.
                        Uri attachmentUri = null;
                        String attachmentType = null;
                        final MessagePartData messagePartData =
                                getMostInterestingAttachment(convMessageData);
                        if (messagePartData != null) {
                            attachmentUri = messagePartData.getContentUri();
                            attachmentType = messagePartData.getContentType();
                        }
                        currConvInfo.mLineInfos.add(new MessageLineInfo(currConvInfo.mIsGroup,
                                authorFullName, authorFirstName, text,
                                attachmentUri, attachmentType, isManualDownloadNeeded, messageId));
                    }
                    messageCount++;
                    currConvInfo.mTotalMessageCount++;
                } while (convMessageCursor.moveToNext());
            }
        } finally {
            if (convMessageCursor != null) {
                convMessageCursor.close();
            }
        }
        if (convLineInfos.isEmpty()) {
            return null;
        } else {
            return new ConversationInfoList(messageCount,
                    Lists.newLinkedList(convLineInfos.values()));
        }
    }

    /**
     * Scans all the attachments for a message and returns the most interesting one that we'll
     * show in a notification. By order of importance, in case there are multiple attachments:
     *      1- an image (because we can show the image as a BigPictureNotification)
     *      2- a video (because we can show a video frame as a BigPictureNotification)
     *      3- a vcard
     *      4- an audio attachment
     * @return MessagePartData for the most interesting part. Can be null.
     */
    private static MessagePartData getMostInterestingAttachment(
            final ConversationMessageData convMessageData) {
        final List<MessagePartData> attachments = convMessageData.getAttachments();

        MessagePartData imagePart = null;
        MessagePartData audioPart = null;
        MessagePartData vcardPart = null;
        MessagePartData videoPart = null;

        // 99.99% of the time there will be 0 or 1 part, since receiving slideshows is so
        // uncommon.

        // Remember the first of each type of part.
        for (final MessagePartData messagePartData : attachments) {
            if (messagePartData.isImage() && imagePart == null) {
                imagePart = messagePartData;
            }
            if (messagePartData.isVideo() && videoPart == null) {
                videoPart = messagePartData;
            }
            if (messagePartData.isVCard() && vcardPart == null) {
                vcardPart = messagePartData;
            }
            if (messagePartData.isAudio() && audioPart == null) {
                audioPart = messagePartData;
            }
        }
        if (imagePart != null) {
            return imagePart;
        } else if (videoPart != null) {
            return videoPart;
        } else if (audioPart != null) {
            return audioPart;
        } else if (vcardPart != null) {
            return vcardPart;
        }
        return null;
    }

    private static int getMaxMessagesInConversationNotification() {
        if (!BugleNotifications.isWearCompanionAppInstalled()) {
            return BugleGservices.get().getInt(
                    BugleGservicesKeys.MAX_MESSAGES_IN_CONVERSATION_NOTIFICATION,
                    BugleGservicesKeys.MAX_MESSAGES_IN_CONVERSATION_NOTIFICATION_DEFAULT);
        }
        return BugleGservices.get().getInt(
                BugleGservicesKeys.MAX_MESSAGES_IN_CONVERSATION_NOTIFICATION_WITH_WEARABLE,
                BugleGservicesKeys.MAX_MESSAGES_IN_CONVERSATION_NOTIFICATION_WITH_WEARABLE_DEFAULT);
    }

    /**
     * Scans the database for messages that need to go into notifications. Creates the appropriate
     * MessageNotificationState depending on if there are multiple senders, or
     * messages from one sender.
     * @return NotificationState for the notification created.
     */
    public static NotificationState getNotificationState() {
        MessageNotificationState state = null;
        final ConversationInfoList convList = createConversationInfoList();

        if (convList == null || convList.mConvInfos.size() == 0) {
            if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
                LogUtil.v(TAG, "MessageNotificationState: No unseen notifications");
            }
        } else {
            final ConversationLineInfo convInfo = convList.mConvInfos.get(0);
            state = new MultiMessageNotificationState(convList);

            if (convList.mConvInfos.size() > 1) {
                // We've got notifications across multiple conversations. Pass in the notification
                // we just built of the most recent notification so we can use that to show the
                // user the new message in the ticker.
                state = new MultiConversationNotificationState(convList, state);
            } else {
                // For now, only show avatars for notifications for a single conversation.
                if (convInfo.mAvatarUri != null) {
                    if (state.mParticipantAvatarsUris == null) {
                        state.mParticipantAvatarsUris = new ArrayList<Uri>(1);
                    }
                    state.mParticipantAvatarsUris.add(convInfo.mAvatarUri);
                }
                if (convInfo.mContactUri != null) {
                    if (state.mParticipantContactUris == null) {
                        state.mParticipantContactUris = new ArrayList<Uri>(1);
                    }
                    state.mParticipantContactUris.add(convInfo.mContactUri);
                }
            }
        }
        if (state != null && LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
            LogUtil.v(TAG, "MessageNotificationState: Notification state created"
                    + ", title = " + LogUtil.sanitizePII(state.mTitle)
                    + ", content = " + LogUtil.sanitizePII(state.mContent.toString()));
        }
        return state;
    }

    protected String getTitle() {
        return mTitle;
    }

    @Override
    public int getLatestMessageNotificationType() {
        // This function is called to determine whether the most recent notification applies
        // to an sms conversation or a hangout conversation. We have different ringtone/vibrate
        // settings for both types of conversations.
        if (mConvList.mConvInfos.size() > 0) {
            final ConversationLineInfo convInfo = mConvList.mConvInfos.get(0);
            return convInfo.getLatestMessageNotificationType();
        }
        return BugleNotifications.LOCAL_SMS_NOTIFICATION;
    }

    @Override
    public String getRingtoneUri() {
        if (mConvList.mConvInfos.size() > 0) {
            return mConvList.mConvInfos.get(0).mRingtoneUri;
        }
        return null;
    }

    @Override
    public boolean getNotificationVibrate() {
        if (mConvList.mConvInfos.size() > 0) {
            return mConvList.mConvInfos.get(0).mNotificationVibrate;
        }
        return false;
    }

    protected CharSequence getTicker() {
        return BugleNotifications.buildColonSeparatedMessage(
                mTickerSender != null ? mTickerSender : mTitle,
                mTickerText != null ? mTickerText : (mTickerNoContent ? null : mContent), null,
                        null);
    }

    private static CharSequence convertHtmlAndStripUrls(final String s) {
        final Spanned text = Html.fromHtml(s);
        if (text instanceof Spannable) {
            stripUrls((Spannable) text);
        }
        return text;
    }

    // Since we don't want to show URLs in notifications, a function
    // to remove them in place.
    private static void stripUrls(final Spannable text) {
        final URLSpan[] spans = text.getSpans(0, text.length(), URLSpan.class);
        for (final URLSpan span : spans) {
            text.removeSpan(span);
        }
    }

    /*
    private static void updateAlertStatusMessages(final long thresholdDeltaMs) {
        // TODO may need this when supporting error notifications
        final EsDatabaseHelper helper = EsDatabaseHelper.getDatabaseHelper();
        final ContentValues values = new ContentValues();
        final long nowMicros = System.currentTimeMillis() * 1000;
        values.put(MessageColumns.ALERT_STATUS, "1");
        final String selection =
                MessageColumns.ALERT_STATUS + "=0 AND (" +
                MessageColumns.STATUS + "=" + EsProvider.MESSAGE_STATUS_FAILED_TO_SEND + " OR (" +
                MessageColumns.STATUS + "!=" + EsProvider.MESSAGE_STATUS_ON_SERVER + " AND " +
                MessageColumns.TIMESTAMP + "+" + thresholdDeltaMs*1000 + "<" + nowMicros + ")) ";

        final int updateCount = helper.getWritableDatabaseWrapper().update(
                EsProvider.MESSAGES_TABLE,
                values,
                selection,
                null);
        if (updateCount > 0) {
            EsConversationsData.notifyConversationsChanged();
        }
    }*/

    static CharSequence applyWarningTextColor(final Context context,
            final CharSequence text) {
        if (text == null) {
            return null;
        }
        final SpannableStringBuilder spanBuilder = new SpannableStringBuilder();
        spanBuilder.append(text);
        spanBuilder.setSpan(new ForegroundColorSpan(context.getResources().getColor(
                R.color.notification_warning_color)), 0, text.length(),
                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        return spanBuilder;
    }

    /**
     * Check for failed messages and post notifications as needed.
     * TODO: Rewrite this as a NotificationState.
     */
    public static void checkFailedMessages() {
        final DatabaseWrapper db = DataModel.get().getDatabase();

        final Cursor messageDataCursor = db.query(DatabaseHelper.MESSAGES_TABLE,
            MessageData.getProjection(),
            FailedMessageQuery.FAILED_MESSAGES_WHERE_CLAUSE,
            null /*selectionArgs*/,
            null /*groupBy*/,
            null /*having*/,
            FailedMessageQuery.FAILED_ORDER_BY);

        try {
            final Context context = Factory.get().getApplicationContext();
            final Resources resources = context.getResources();
            final NotificationManagerCompat notificationManager =
                    NotificationManagerCompat.from(context);
            if (messageDataCursor != null) {
                final MessageData messageData = new MessageData();

                final HashSet<String> conversationsWithFailedMessages = new HashSet<String>();

                // track row ids in case we want to display something that requires this
                // information
                final ArrayList<Integer> failedMessages = new ArrayList<Integer>();

                int cursorPosition = -1;
                final long when = 0;

                messageDataCursor.moveToPosition(-1);
                while (messageDataCursor.moveToNext()) {
                    messageData.bind(messageDataCursor);

                    final String conversationId = messageData.getConversationId();
                    if (DataModel.get().isNewMessageObservable(conversationId)) {
                        // Don't post a system notification for an observable conversation
                        // because we already show an angry red annotation in the conversation
                        // itself or in the conversation preview snippet.
                        continue;
                    }

                    cursorPosition = messageDataCursor.getPosition();
                    failedMessages.add(cursorPosition);
                    conversationsWithFailedMessages.add(conversationId);
                }

                if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
                    LogUtil.d(TAG, "Found " + failedMessages.size() + " failed messages");
                }
                if (failedMessages.size() > 0) {
                    final NotificationCompat.Builder builder =
                            new NotificationCompat.Builder(context);

                    CharSequence line1;
                    CharSequence line2;
                    final boolean isRichContent = false;
                    ConversationIdSet conversationIds = null;
                    PendingIntent destinationIntent;
                    if (failedMessages.size() == 1) {
                        messageDataCursor.moveToPosition(cursorPosition);
                        messageData.bind(messageDataCursor);
                        final String conversationId =  messageData.getConversationId();

                        // We have a single conversation, go directly to that conversation.
                        destinationIntent = UIIntents.get()
                                .getPendingIntentForConversationActivity(context,
                                        conversationId,
                                        null /*draft*/);

                        conversationIds = ConversationIdSet.createSet(conversationId);

                        final String failedMessgeSnippet = messageData.getMessageText();
                        int failureStringId;
                        if (messageData.getStatus() ==
                                MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED) {
                            failureStringId =
                                    R.string.notification_download_failures_line1_singular;
                        } else {
                            failureStringId = R.string.notification_send_failures_line1_singular;
                        }
                        line1 = resources.getString(failureStringId);
                        line2 = failedMessgeSnippet;
                        // Set rich text for non-SMS messages or MMS push notification messages
                        // which we generate locally with rich text
                        // TODO- fix this
//                        if (messageData.isMmsInd()) {
//                            isRichContent = true;
//                        }
                    } else {
                        // We have notifications for multiple conversation, go to the conversation
                        // list.
                        destinationIntent = UIIntents.get()
                            .getPendingIntentForConversationListActivity(context);

                        int line1StringId;
                        int line2PluralsId;
                        if (messageData.getStatus() ==
                                MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED) {
                            line1StringId =
                                    R.string.notification_download_failures_line1_plural;
                            line2PluralsId = R.plurals.notification_download_failures;
                        } else {
                            line1StringId = R.string.notification_send_failures_line1_plural;
                            line2PluralsId = R.plurals.notification_send_failures;
                        }
                        line1 = resources.getString(line1StringId);
                        line2 = resources.getQuantityString(
                                line2PluralsId,
                                conversationsWithFailedMessages.size(),
                                failedMessages.size(),
                                conversationsWithFailedMessages.size());
                    }
                    line1 = applyWarningTextColor(context, line1);
                    line2 = applyWarningTextColor(context, line2);

                    final PendingIntent pendingIntentForDelete =
                            UIIntents.get().getPendingIntentForClearingNotifications(
                                    context,
                                    BugleNotifications.UPDATE_ERRORS,
                                    conversationIds,
                                    0);

                    builder
                        .setContentTitle(line1)
                        .setTicker(line1)
                        .setWhen(when > 0 ? when : System.currentTimeMillis())
                        .setSmallIcon(R.drawable.ic_failed_light)
                        .setDeleteIntent(pendingIntentForDelete)
                        .setContentIntent(destinationIntent)
                        .setSound(UriUtil.getUriForResourceId(context, R.raw.message_failure));
                    if (isRichContent && !TextUtils.isEmpty(line2)) {
                        final NotificationCompat.InboxStyle inboxStyle =
                                new NotificationCompat.InboxStyle(builder);
                        if (line2 != null) {
                            inboxStyle.addLine(Html.fromHtml(line2.toString()));
                        }
                        builder.setStyle(inboxStyle);
                    } else {
                        builder.setContentText(line2);
                    }

                    if (builder != null) {
                        notificationManager.notify(
                                BugleNotifications.buildNotificationTag(
                                        PendingIntentConstants.MSG_SEND_ERROR, null),
                                PendingIntentConstants.MSG_SEND_ERROR,
                                builder.build());
                    }
                } else {
                    notificationManager.cancel(
                            BugleNotifications.buildNotificationTag(
                                    PendingIntentConstants.MSG_SEND_ERROR, null),
                            PendingIntentConstants.MSG_SEND_ERROR);
                }
            }
        } finally {
            if (messageDataCursor != null) {
                messageDataCursor.close();
            }
        }
    }
}