summaryrefslogtreecommitdiffstats
path: root/src/com/android/messaging/datamodel/data/ConversationMessageData.java
blob: 19e1b977e0f8d8b449b29e0ca0b3d93241bf1311 (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
/*
 * 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.data;

import android.database.Cursor;
import android.net.Uri;
import android.provider.BaseColumns;
import android.provider.ContactsContract;
import android.text.TextUtils;
import android.text.format.DateUtils;

import com.android.messaging.datamodel.DatabaseHelper;
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
import com.android.messaging.datamodel.DatabaseHelper.PartColumns;
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
import com.android.messaging.util.Assert;
import com.android.messaging.util.BugleGservices;
import com.android.messaging.util.BugleGservicesKeys;
import com.android.messaging.util.ContentType;
import com.android.messaging.util.Dates;
import com.android.messaging.util.LogUtil;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Predicate;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

/**
 * Class representing a message within a conversation sequence. The message parts
 * are available via the getParts() method.
 *
 * TODO: See if we can delegate to MessageData for the logic that this class duplicates
 * (e.g. getIsMms).
 */
public class ConversationMessageData {
    private static final String TAG = LogUtil.BUGLE_TAG;

    private String mMessageId;
    private String mConversationId;
    private String mParticipantId;
    private int mPartsCount;
    private List<MessagePartData> mParts;
    private long mSentTimestamp;
    private long mReceivedTimestamp;
    private boolean mSeen;
    private boolean mRead;
    private int mProtocol;
    private int mStatus;
    private String mSmsMessageUri;
    private int mSmsPriority;
    private int mSmsMessageSize;
    private String mMmsSubject;
    private long mMmsExpiry;
    private int mRawTelephonyStatus;
    private String mSenderFullName;
    private String mSenderFirstName;
    private String mSenderDisplayDestination;
    private String mSenderNormalizedDestination;
    private String mSenderProfilePhotoUri;
    private long mSenderContactId;
    private String mSenderContactLookupKey;
    private String mSelfParticipantId;

    /** Are we similar enough to the previous/next messages that we can cluster them? */
    private boolean mCanClusterWithPreviousMessage;
    private boolean mCanClusterWithNextMessage;

    public ConversationMessageData() {
    }

    public void bind(final Cursor cursor) {
        mMessageId = cursor.getString(INDEX_MESSAGE_ID);
        mConversationId = cursor.getString(INDEX_CONVERSATION_ID);
        mParticipantId = cursor.getString(INDEX_PARTICIPANT_ID);
        mPartsCount = cursor.getInt(INDEX_PARTS_COUNT);

        mParts = makeParts(
                cursor.getString(INDEX_PARTS_IDS),
                cursor.getString(INDEX_PARTS_CONTENT_TYPES),
                cursor.getString(INDEX_PARTS_CONTENT_URIS),
                cursor.getString(INDEX_PARTS_WIDTHS),
                cursor.getString(INDEX_PARTS_HEIGHTS),
                cursor.getString(INDEX_PARTS_TEXTS),
                mPartsCount,
                mMessageId);

        mSentTimestamp = cursor.getLong(INDEX_SENT_TIMESTAMP);
        mReceivedTimestamp = cursor.getLong(INDEX_RECEIVED_TIMESTAMP);
        mSeen = (cursor.getInt(INDEX_SEEN) != 0);
        mRead = (cursor.getInt(INDEX_READ) != 0);
        mProtocol = cursor.getInt(INDEX_PROTOCOL);
        mStatus = cursor.getInt(INDEX_STATUS);
        mSmsMessageUri = cursor.getString(INDEX_SMS_MESSAGE_URI);
        mSmsPriority = cursor.getInt(INDEX_SMS_PRIORITY);
        mSmsMessageSize = cursor.getInt(INDEX_SMS_MESSAGE_SIZE);
        mMmsSubject = cursor.getString(INDEX_MMS_SUBJECT);
        mMmsExpiry = cursor.getLong(INDEX_MMS_EXPIRY);
        mRawTelephonyStatus = cursor.getInt(INDEX_RAW_TELEPHONY_STATUS);
        mSenderFullName = cursor.getString(INDEX_SENDER_FULL_NAME);
        mSenderFirstName = cursor.getString(INDEX_SENDER_FIRST_NAME);
        mSenderDisplayDestination = cursor.getString(INDEX_SENDER_DISPLAY_DESTINATION);
        mSenderNormalizedDestination = cursor.getString(INDEX_SENDER_NORMALIZED_DESTINATION);
        mSenderProfilePhotoUri = cursor.getString(INDEX_SENDER_PROFILE_PHOTO_URI);
        mSenderContactId = cursor.getLong(INDEX_SENDER_CONTACT_ID);
        mSenderContactLookupKey = cursor.getString(INDEX_SENDER_CONTACT_LOOKUP_KEY);
        mSelfParticipantId = cursor.getString(INDEX_SELF_PARTICIPIANT_ID);

        if (!cursor.isFirst() && cursor.moveToPrevious()) {
            mCanClusterWithPreviousMessage = canClusterWithMessage(cursor);
            cursor.moveToNext();
        } else {
            mCanClusterWithPreviousMessage = false;
        }
        if (!cursor.isLast() && cursor.moveToNext()) {
            mCanClusterWithNextMessage = canClusterWithMessage(cursor);
            cursor.moveToPrevious();
        } else {
            mCanClusterWithNextMessage = false;
        }
    }

    private boolean canClusterWithMessage(final Cursor cursor) {
        final String otherParticipantId = cursor.getString(INDEX_PARTICIPANT_ID);
        if (!TextUtils.equals(getParticipantId(), otherParticipantId)) {
            return false;
        }
        final int otherStatus = cursor.getInt(INDEX_STATUS);
        final boolean otherIsIncoming = (otherStatus >= MessageData.BUGLE_STATUS_FIRST_INCOMING);
        if (getIsIncoming() != otherIsIncoming) {
            return false;
        }
        final long otherReceivedTimestamp = cursor.getLong(INDEX_RECEIVED_TIMESTAMP);
        final long timestampDeltaMillis = Math.abs(mReceivedTimestamp - otherReceivedTimestamp);
        if (timestampDeltaMillis > DateUtils.MINUTE_IN_MILLIS) {
            return false;
        }
        final String otherSelfId = cursor.getString(INDEX_SELF_PARTICIPIANT_ID);
        if (!TextUtils.equals(getSelfParticipantId(), otherSelfId)) {
            return false;
        }
        return true;
    }

    private static final Character QUOTE_CHAR = '\'';
    private static final char DIVIDER = '|';

    // statics to avoid unnecessary object allocation
    private static final StringBuilder sUnquoteStringBuilder = new StringBuilder();
    private static final ArrayList<String> sUnquoteResults = new ArrayList<String>();

    // this lock is used to guard access to the above statics
    private static final Object sUnquoteLock = new Object();

    private static void addResult(final ArrayList<String> results, final StringBuilder value) {
        if (value.length() > 0) {
            results.add(value.toString());
        } else {
            results.add(EMPTY_STRING);
        }
    }

    @VisibleForTesting
    static String[] splitUnquotedString(final String inputString) {
        if (TextUtils.isEmpty(inputString)) {
            return new String[0];
        }

        return inputString.split("\\" + DIVIDER);
    }

    /**
     * Takes a group-concated and quoted string and decomposes it into its constituent
     * parts.  A quoted string starts and ends with a single quote.  Actual single quotes
     * within the string are escaped using a second single quote.  So, for example, an
     * input string with 3 constituent parts might look like this:
     *
     * 'now is the time'|'I can''t do it'|'foo'
     *
     * This would be returned as an array of 3 strings as follows:
     * now is the time
     * I can't do it
     * foo
     *
     * This is achieved by walking through the inputString, character by character,
     * ignoring the outer quotes and the divider and replacing any pair of consecutive
     * single quotes with a single single quote.
     *
     * @param inputString
     * @return array of constituent strings
     */
    @VisibleForTesting
    static String[] splitQuotedString(final String inputString) {
        if (TextUtils.isEmpty(inputString)) {
            return new String[0];
        }

        // this method can be called from multiple threads but it uses a static
        // string builder
        synchronized (sUnquoteLock) {
            final int length = inputString.length();
            final ArrayList<String> results = sUnquoteResults;
            results.clear();

            int characterPos = -1;
            while (++characterPos < length) {
                final char mustBeQuote = inputString.charAt(characterPos);
                Assert.isTrue(QUOTE_CHAR == mustBeQuote);
                while (++characterPos < length) {
                    final char currentChar = inputString.charAt(characterPos);
                    if (currentChar == QUOTE_CHAR) {
                        final char peekAhead = characterPos < length - 1
                                ? inputString.charAt(characterPos + 1) : 0;

                        if (peekAhead == QUOTE_CHAR) {
                            characterPos += 1;  // skip the second quote
                        } else {
                            addResult(results, sUnquoteStringBuilder);
                            sUnquoteStringBuilder.setLength(0);

                            Assert.isTrue((peekAhead == DIVIDER) || (peekAhead == (char) 0));
                            characterPos += 1;  // skip the divider
                            break;
                        }
                    }
                    sUnquoteStringBuilder.append(currentChar);
                }
            }
            return results.toArray(new String[results.size()]);
        }
    }

    static MessagePartData makePartData(
            final String partId,
            final String contentType,
            final String contentUriString,
            final String contentWidth,
            final String contentHeight,
            final String text,
            final String messageId) {
        if (ContentType.isTextType(contentType)) {
            final MessagePartData textPart = MessagePartData.createTextMessagePart(text);
            textPart.updatePartId(partId);
            textPart.updateMessageId(messageId);
            return textPart;
        } else {
            final Uri contentUri = Uri.parse(contentUriString);
            final int width = Integer.parseInt(contentWidth);
            final int height = Integer.parseInt(contentHeight);
            final MessagePartData attachmentPart = MessagePartData.createMediaMessagePart(
                    contentType, contentUri, width, height);
            attachmentPart.updatePartId(partId);
            attachmentPart.updateMessageId(messageId);
            return attachmentPart;
        }
    }

    @VisibleForTesting
    static List<MessagePartData> makeParts(
            final String rawIds,
            final String rawContentTypes,
            final String rawContentUris,
            final String rawWidths,
            final String rawHeights,
            final String rawTexts,
            final int partsCount,
            final String messageId) {
        final List<MessagePartData> parts = new LinkedList<MessagePartData>();
        if (partsCount == 1) {
            parts.add(makePartData(
                    rawIds,
                    rawContentTypes,
                    rawContentUris,
                    rawWidths,
                    rawHeights,
                    rawTexts,
                    messageId));
        } else {
            unpackMessageParts(
                    parts,
                    splitUnquotedString(rawIds),
                    splitQuotedString(rawContentTypes),
                    splitQuotedString(rawContentUris),
                    splitUnquotedString(rawWidths),
                    splitUnquotedString(rawHeights),
                    splitQuotedString(rawTexts),
                    partsCount,
                    messageId);
        }
        return parts;
    }

    @VisibleForTesting
    static void unpackMessageParts(
            final List<MessagePartData> parts,
            final String[] ids,
            final String[] contentTypes,
            final String[] contentUris,
            final String[] contentWidths,
            final String[] contentHeights,
            final String[] texts,
            final int partsCount,
            final String messageId) {

        Assert.equals(partsCount, ids.length);
        Assert.equals(partsCount, contentTypes.length);
        Assert.equals(partsCount, contentUris.length);
        Assert.equals(partsCount, contentWidths.length);
        Assert.equals(partsCount, contentHeights.length);
        Assert.equals(partsCount, texts.length);

        for (int i = 0; i < partsCount; i++) {
            parts.add(makePartData(
                    ids[i],
                    contentTypes[i],
                    contentUris[i],
                    contentWidths[i],
                    contentHeights[i],
                    texts[i],
                    messageId));
        }

        if (parts.size() != partsCount) {
            LogUtil.wtf(TAG, "Only unpacked " + parts.size() + " parts from message (id="
                    + messageId + "), expected " + partsCount + " parts");
        }
    }

    public final String getMessageId() {
        return mMessageId;
    }

    public final String getConversationId() {
        return mConversationId;
    }

    public final String getParticipantId() {
        return mParticipantId;
    }

    public List<MessagePartData> getParts() {
        return mParts;
    }

    public boolean hasText() {
        for (final MessagePartData part : mParts) {
            if (part.isText()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Get a concatenation of all text parts
     *
     * @return the text that is a concatenation of all text parts
     */
    public String getText() {
        // This is optimized for single text part case, which is the majority

        // For single text part, we just return the part without creating the StringBuilder
        String firstTextPart = null;
        boolean foundText = false;
        // For multiple text parts, we need the StringBuilder and the separator for concatenation
        StringBuilder sb = null;
        String separator = null;
        for (final MessagePartData part : mParts) {
            if (part.isText()) {
                if (!foundText) {
                    // First text part
                    firstTextPart = part.getText();
                    foundText = true;
                } else {
                    // Second and beyond
                    if (sb == null) {
                        // Need the StringBuilder and the separator starting from 2nd text part
                        sb = new StringBuilder();
                        if (!TextUtils.isEmpty(firstTextPart)) {
                              sb.append(firstTextPart);
                        }
                        separator = BugleGservices.get().getString(
                                BugleGservicesKeys.MMS_TEXT_CONCAT_SEPARATOR,
                                BugleGservicesKeys.MMS_TEXT_CONCAT_SEPARATOR_DEFAULT);
                    }
                    final String partText = part.getText();
                    if (!TextUtils.isEmpty(partText)) {
                        if (!TextUtils.isEmpty(separator) && sb.length() > 0) {
                            sb.append(separator);
                        }
                        sb.append(partText);
                    }
                }
            }
        }
        if (sb == null) {
            // Only one text part
            return firstTextPart;
        } else {
            // More than one
            return sb.toString();
        }
    }

    public boolean hasAttachments() {
        for (final MessagePartData part : mParts) {
            if (part.isAttachment()) {
                return true;
            }
        }
        return false;
    }

    public List<MessagePartData> getAttachments() {
        return getAttachments(null);
    }

    public List<MessagePartData> getAttachments(final Predicate<MessagePartData> filter) {
        if (mParts.isEmpty()) {
            return Collections.emptyList();
        }
        final List<MessagePartData> attachmentParts = new LinkedList<>();
        for (final MessagePartData part : mParts) {
            if (part.isAttachment()) {
                if (filter == null || filter.apply(part)) {
                    attachmentParts.add(part);
                }
            }
        }
        return attachmentParts;
    }

    public final long getSentTimeStamp() {
        return mSentTimestamp;
    }

    public final long getReceivedTimeStamp() {
        return mReceivedTimestamp;
    }

    public final String getFormattedReceivedTimeStamp() {
        return Dates.getMessageTimeString(mReceivedTimestamp).toString();
    }

    public final boolean getIsSeen() {
        return mSeen;
    }

    public final boolean getIsRead() {
        return mRead;
    }

    public final boolean getIsMms() {
        return (mProtocol == MessageData.PROTOCOL_MMS ||
                mProtocol == MessageData.PROTOCOL_MMS_PUSH_NOTIFICATION);
    }

    public final boolean getIsMmsNotification() {
        return (mProtocol == MessageData.PROTOCOL_MMS_PUSH_NOTIFICATION);
    }

    public final boolean getIsSms() {
        return mProtocol == (MessageData.PROTOCOL_SMS);
    }

    final int getProtocol() {
        return mProtocol;
    }

    public final int getStatus() {
        return mStatus;
    }

    public final String getSmsMessageUri() {
        return mSmsMessageUri;
    }

    public final int getSmsPriority() {
        return mSmsPriority;
    }

    public final int getSmsMessageSize() {
        return mSmsMessageSize;
    }

    public final String getMmsSubject() {
        return mMmsSubject;
    }

    public final long getMmsExpiry() {
        return mMmsExpiry;
    }

    public final int getRawTelephonyStatus() {
        return mRawTelephonyStatus;
    }

    public final String getSelfParticipantId() {
        return mSelfParticipantId;
    }

    public boolean getIsIncoming() {
        return (mStatus >= MessageData.BUGLE_STATUS_FIRST_INCOMING);
    }

    public boolean hasIncomingErrorStatus() {
        return (mStatus == MessageData.BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE ||
                mStatus == MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED);
    }

    public boolean getIsSendComplete() {
        return mStatus == MessageData.BUGLE_STATUS_OUTGOING_COMPLETE;
    }

    public String getSenderFullName() {
        return mSenderFullName;
    }

    public String getSenderFirstName() {
        return mSenderFirstName;
    }

    public String getSenderDisplayDestination() {
        return mSenderDisplayDestination;
    }

    public String getSenderNormalizedDestination() {
        return mSenderNormalizedDestination;
    }

    public Uri getSenderProfilePhotoUri() {
        return mSenderProfilePhotoUri == null ? null : Uri.parse(mSenderProfilePhotoUri);
    }

    public long getSenderContactId() {
        return mSenderContactId;
    }

    public String getSenderDisplayName() {
        if (!TextUtils.isEmpty(mSenderFullName)) {
            return mSenderFullName;
        }
        if (!TextUtils.isEmpty(mSenderFirstName)) {
            return mSenderFirstName;
        }
        return mSenderDisplayDestination;
    }

    public String getSenderContactLookupKey() {
        return mSenderContactLookupKey;
    }

    public boolean getShowDownloadMessage() {
        return MessageData.getShowDownloadMessage(mStatus);
    }

    public boolean getShowResendMessage() {
        return MessageData.getShowResendMessage(mStatus);
    }

    public boolean getCanForwardMessage() {
        // Even for outgoing messages, we only allow forwarding if the message has finished sending
        // as media often has issues when send isn't complete
        return (mStatus == MessageData.BUGLE_STATUS_OUTGOING_COMPLETE ||
                mStatus == MessageData.BUGLE_STATUS_INCOMING_COMPLETE);
    }

    public boolean getCanCopyMessageToClipboard() {
        return (hasText() &&
                (!getIsIncoming() || mStatus == MessageData.BUGLE_STATUS_INCOMING_COMPLETE));
    }

    public boolean getOneClickResendMessage() {
        return MessageData.getOneClickResendMessage(mStatus, mRawTelephonyStatus);
    }

    /**
     * Get sender's lookup uri.
     * This method doesn't support corp contacts.
     *
     * @return Lookup uri of sender's contact
     */
    public Uri getSenderContactLookupUri() {
        if (mSenderContactId > ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED
                && !TextUtils.isEmpty(mSenderContactLookupKey)) {
            return ContactsContract.Contacts.getLookupUri(mSenderContactId,
                    mSenderContactLookupKey);
        }
        return null;
    }

    public boolean getCanClusterWithPreviousMessage() {
        return mCanClusterWithPreviousMessage;
    }

    public boolean getCanClusterWithNextMessage() {
        return mCanClusterWithNextMessage;
    }

    @Override
    public String toString() {
        return MessageData.toString(mMessageId, mParts);
    }

    // Data definitions

    public static final String getConversationMessagesQuerySql() {
        return CONVERSATION_MESSAGES_QUERY_SQL
                + " AND "
                // Inject the conversation id
                + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?)"
                + CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY;
    }

    static final String getConversationMessageIdsQuerySql() {
        return CONVERSATION_MESSAGES_IDS_QUERY_SQL
                + " AND "
                // Inject the conversation id
                + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?)"
                + CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY;
    }

    public static final String getNotificationQuerySql() {
        return CONVERSATION_MESSAGES_QUERY_SQL
                + " AND "
                + "(" + DatabaseHelper.MessageColumns.STATUS + " in ("
                + MessageData.BUGLE_STATUS_INCOMING_COMPLETE + ", "
                + MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD + ")"
                + " AND "
                + DatabaseHelper.MessageColumns.SEEN + " = 0)"
                + ")"
                + NOTIFICATION_QUERY_SQL_GROUP_BY;
    }

    public static final String getWearableQuerySql() {
        return CONVERSATION_MESSAGES_QUERY_SQL
                + " AND "
                + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?"
                + " AND "
                + DatabaseHelper.MessageColumns.STATUS + " IN ("
                + MessageData.BUGLE_STATUS_OUTGOING_DELIVERED + ", "
                + MessageData.BUGLE_STATUS_OUTGOING_COMPLETE + ", "
                + MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND + ", "
                + MessageData.BUGLE_STATUS_OUTGOING_SENDING + ", "
                + MessageData.BUGLE_STATUS_OUTGOING_RESENDING + ", "
                + MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY + ", "
                + MessageData.BUGLE_STATUS_INCOMING_COMPLETE + ", "
                + MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD + ")"
                + ")"
                + NOTIFICATION_QUERY_SQL_GROUP_BY;
    }

    /*
     * Generate a sqlite snippet to call the quote function on the columnName argument.
     * The columnName doesn't strictly have to be a column name (e.g. it could be an
     * expression).
     */
    private static String quote(final String columnName) {
        return "quote(" + columnName + ")";
    }

    private static String makeGroupConcatString(final String column) {
        return "group_concat(" + column + ", '" + DIVIDER + "')";
    }

    private static String makeIfNullString(final String column) {
        return "ifnull(" + column + "," + "''" + ")";
    }

    private static String makePartsTableColumnString(final String column) {
        return DatabaseHelper.PARTS_TABLE + '.' + column;
    }

    private static String makeCaseWhenString(final String column,
                                             final boolean quote,
                                             final String asColumn) {
        final String fullColumn = makeIfNullString(makePartsTableColumnString(column));
        final String groupConcatTerm = quote
                ? makeGroupConcatString(quote(fullColumn))
                : makeGroupConcatString(fullColumn);
        return "CASE WHEN (" + CONVERSATION_MESSAGE_VIEW_PARTS_COUNT + ">1) THEN " + groupConcatTerm
                + " ELSE " + makePartsTableColumnString(column) + " END AS " + asColumn;
    }

    private static final String CONVERSATION_MESSAGE_VIEW_PARTS_COUNT =
            "count(" + DatabaseHelper.PARTS_TABLE + '.' + PartColumns._ID + ")";

    private static final String EMPTY_STRING = "";

    private static final String CONVERSATION_MESSAGES_QUERY_PROJECTION_SQL =
            DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns._ID
            + " as " + ConversationMessageViewColumns._ID + ", "
            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.CONVERSATION_ID
            + " as " + ConversationMessageViewColumns.CONVERSATION_ID + ", "
            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SENDER_PARTICIPANT_ID
            + " as " + ConversationMessageViewColumns.PARTICIPANT_ID + ", "

            + makeCaseWhenString(PartColumns._ID, false,
                    ConversationMessageViewColumns.PARTS_IDS) + ", "
            + makeCaseWhenString(PartColumns.CONTENT_TYPE, true,
                    ConversationMessageViewColumns.PARTS_CONTENT_TYPES) + ", "
            + makeCaseWhenString(PartColumns.CONTENT_URI, true,
                    ConversationMessageViewColumns.PARTS_CONTENT_URIS) + ", "
            + makeCaseWhenString(PartColumns.WIDTH, false,
                    ConversationMessageViewColumns.PARTS_WIDTHS) + ", "
            + makeCaseWhenString(PartColumns.HEIGHT, false,
                    ConversationMessageViewColumns.PARTS_HEIGHTS) + ", "
            + makeCaseWhenString(PartColumns.TEXT, true,
                    ConversationMessageViewColumns.PARTS_TEXTS) + ", "

            + CONVERSATION_MESSAGE_VIEW_PARTS_COUNT
            + " as " + ConversationMessageViewColumns.PARTS_COUNT + ", "

            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SENT_TIMESTAMP
            + " as " + ConversationMessageViewColumns.SENT_TIMESTAMP + ", "
            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP
            + " as " + ConversationMessageViewColumns.RECEIVED_TIMESTAMP + ", "
            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SEEN
            + " as " + ConversationMessageViewColumns.SEEN + ", "
            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.READ
            + " as " + ConversationMessageViewColumns.READ + ", "
            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.PROTOCOL
            + " as " + ConversationMessageViewColumns.PROTOCOL + ", "
            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.STATUS
            + " as " + ConversationMessageViewColumns.STATUS + ", "
            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SMS_MESSAGE_URI
            + " as " + ConversationMessageViewColumns.SMS_MESSAGE_URI + ", "
            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SMS_PRIORITY
            + " as " + ConversationMessageViewColumns.SMS_PRIORITY + ", "
            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SMS_MESSAGE_SIZE
            + " as " + ConversationMessageViewColumns.SMS_MESSAGE_SIZE + ", "
            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.MMS_SUBJECT
            + " as " + ConversationMessageViewColumns.MMS_SUBJECT + ", "
            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.MMS_EXPIRY
            + " as " + ConversationMessageViewColumns.MMS_EXPIRY + ", "
            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RAW_TELEPHONY_STATUS
            + " as " + ConversationMessageViewColumns.RAW_TELEPHONY_STATUS + ", "
            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SELF_PARTICIPANT_ID
            + " as " + ConversationMessageViewColumns.SELF_PARTICIPANT_ID + ", "
            + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.FULL_NAME
            + " as " + ConversationMessageViewColumns.SENDER_FULL_NAME + ", "
            + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.FIRST_NAME
            + " as " + ConversationMessageViewColumns.SENDER_FIRST_NAME + ", "
            + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.DISPLAY_DESTINATION
            + " as " + ConversationMessageViewColumns.SENDER_DISPLAY_DESTINATION + ", "
            + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.NORMALIZED_DESTINATION
            + " as " + ConversationMessageViewColumns.SENDER_NORMALIZED_DESTINATION + ", "
            + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.PROFILE_PHOTO_URI
            + " as " + ConversationMessageViewColumns.SENDER_PROFILE_PHOTO_URI + ", "
            + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.CONTACT_ID
            + " as " + ConversationMessageViewColumns.SENDER_CONTACT_ID + ", "
            + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.LOOKUP_KEY
            + " as " + ConversationMessageViewColumns.SENDER_CONTACT_LOOKUP_KEY + " ";

    private static final String CONVERSATION_MESSAGES_QUERY_FROM_WHERE_SQL =
            " FROM " + DatabaseHelper.MESSAGES_TABLE
            + " LEFT JOIN " + DatabaseHelper.PARTS_TABLE
            + " ON (" + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns._ID
            + "=" + DatabaseHelper.PARTS_TABLE + "." + PartColumns.MESSAGE_ID + ") "
            + " LEFT JOIN " + DatabaseHelper.PARTICIPANTS_TABLE
            + " ON (" + DatabaseHelper.MESSAGES_TABLE + '.' +  MessageColumns.SENDER_PARTICIPANT_ID
            + '=' + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns._ID + ")"
            // Exclude draft messages from main view
            + " WHERE (" + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.STATUS
            + " <> " + MessageData.BUGLE_STATUS_OUTGOING_DRAFT;

    // This query is mostly static, except for the injection of conversation id. This is for
    // performance reasons, to ensure that the query uses indices and does not trigger full scans
    // of the messages table. See b/17160946 for more details.
    private static final String CONVERSATION_MESSAGES_QUERY_SQL = "SELECT "
            + CONVERSATION_MESSAGES_QUERY_PROJECTION_SQL
            + CONVERSATION_MESSAGES_QUERY_FROM_WHERE_SQL;

    private static final String CONVERSATION_MESSAGE_IDS_PROJECTION_SQL =
            DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns._ID
                    + " as " + ConversationMessageViewColumns._ID + " ";

    private static final String CONVERSATION_MESSAGES_IDS_QUERY_SQL = "SELECT "
            + CONVERSATION_MESSAGE_IDS_PROJECTION_SQL
            + CONVERSATION_MESSAGES_QUERY_FROM_WHERE_SQL;

    // Note that we sort DESC and ConversationData reverses the cursor.  This is a performance
    // issue (improvement) for large cursors.
    private static final String CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY =
            " GROUP BY " + DatabaseHelper.PARTS_TABLE + '.' + PartColumns.MESSAGE_ID
          + " ORDER BY "
          + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP + " DESC";

    private static final String NOTIFICATION_QUERY_SQL_GROUP_BY =
            " GROUP BY " + DatabaseHelper.PARTS_TABLE + '.' + PartColumns.MESSAGE_ID
          + " ORDER BY "
          + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP + " DESC";

    interface ConversationMessageViewColumns extends BaseColumns {
        static final String _ID = MessageColumns._ID;
        static final String CONVERSATION_ID = MessageColumns.CONVERSATION_ID;
        static final String PARTICIPANT_ID = MessageColumns.SENDER_PARTICIPANT_ID;
        static final String PARTS_COUNT = "parts_count";
        static final String SENT_TIMESTAMP = MessageColumns.SENT_TIMESTAMP;
        static final String RECEIVED_TIMESTAMP = MessageColumns.RECEIVED_TIMESTAMP;
        static final String SEEN = MessageColumns.SEEN;
        static final String READ = MessageColumns.READ;
        static final String PROTOCOL = MessageColumns.PROTOCOL;
        static final String STATUS = MessageColumns.STATUS;
        static final String SMS_MESSAGE_URI = MessageColumns.SMS_MESSAGE_URI;
        static final String SMS_PRIORITY = MessageColumns.SMS_PRIORITY;
        static final String SMS_MESSAGE_SIZE = MessageColumns.SMS_MESSAGE_SIZE;
        static final String MMS_SUBJECT = MessageColumns.MMS_SUBJECT;
        static final String MMS_EXPIRY = MessageColumns.MMS_EXPIRY;
        static final String RAW_TELEPHONY_STATUS = MessageColumns.RAW_TELEPHONY_STATUS;
        static final String SELF_PARTICIPANT_ID = MessageColumns.SELF_PARTICIPANT_ID;
        static final String SENDER_FULL_NAME = ParticipantColumns.FULL_NAME;
        static final String SENDER_FIRST_NAME = ParticipantColumns.FIRST_NAME;
        static final String SENDER_DISPLAY_DESTINATION = ParticipantColumns.DISPLAY_DESTINATION;
        static final String SENDER_NORMALIZED_DESTINATION =
                ParticipantColumns.NORMALIZED_DESTINATION;
        static final String SENDER_PROFILE_PHOTO_URI = ParticipantColumns.PROFILE_PHOTO_URI;
        static final String SENDER_CONTACT_ID = ParticipantColumns.CONTACT_ID;
        static final String SENDER_CONTACT_LOOKUP_KEY = ParticipantColumns.LOOKUP_KEY;
        static final String PARTS_IDS = "parts_ids";
        static final String PARTS_CONTENT_TYPES = "parts_content_types";
        static final String PARTS_CONTENT_URIS = "parts_content_uris";
        static final String PARTS_WIDTHS = "parts_widths";
        static final String PARTS_HEIGHTS = "parts_heights";
        static final String PARTS_TEXTS = "parts_texts";
    }

    private static int sIndexIncrementer = 0;

    private static final int INDEX_MESSAGE_ID                    = sIndexIncrementer++;
    private static final int INDEX_CONVERSATION_ID               = sIndexIncrementer++;
    private static final int INDEX_PARTICIPANT_ID                = sIndexIncrementer++;

    private static final int INDEX_PARTS_IDS                     = sIndexIncrementer++;
    private static final int INDEX_PARTS_CONTENT_TYPES           = sIndexIncrementer++;
    private static final int INDEX_PARTS_CONTENT_URIS            = sIndexIncrementer++;
    private static final int INDEX_PARTS_WIDTHS                  = sIndexIncrementer++;
    private static final int INDEX_PARTS_HEIGHTS                 = sIndexIncrementer++;
    private static final int INDEX_PARTS_TEXTS                   = sIndexIncrementer++;

    private static final int INDEX_PARTS_COUNT                   = sIndexIncrementer++;

    private static final int INDEX_SENT_TIMESTAMP                = sIndexIncrementer++;
    private static final int INDEX_RECEIVED_TIMESTAMP            = sIndexIncrementer++;
    private static final int INDEX_SEEN                          = sIndexIncrementer++;
    private static final int INDEX_READ                          = sIndexIncrementer++;
    private static final int INDEX_PROTOCOL                      = sIndexIncrementer++;
    private static final int INDEX_STATUS                        = sIndexIncrementer++;
    private static final int INDEX_SMS_MESSAGE_URI               = sIndexIncrementer++;
    private static final int INDEX_SMS_PRIORITY                  = sIndexIncrementer++;
    private static final int INDEX_SMS_MESSAGE_SIZE              = sIndexIncrementer++;
    private static final int INDEX_MMS_SUBJECT                   = sIndexIncrementer++;
    private static final int INDEX_MMS_EXPIRY                    = sIndexIncrementer++;
    private static final int INDEX_RAW_TELEPHONY_STATUS          = sIndexIncrementer++;
    private static final int INDEX_SELF_PARTICIPIANT_ID          = sIndexIncrementer++;
    private static final int INDEX_SENDER_FULL_NAME              = sIndexIncrementer++;
    private static final int INDEX_SENDER_FIRST_NAME             = sIndexIncrementer++;
    private static final int INDEX_SENDER_DISPLAY_DESTINATION    = sIndexIncrementer++;
    private static final int INDEX_SENDER_NORMALIZED_DESTINATION = sIndexIncrementer++;
    private static final int INDEX_SENDER_PROFILE_PHOTO_URI      = sIndexIncrementer++;
    private static final int INDEX_SENDER_CONTACT_ID             = sIndexIncrementer++;
    private static final int INDEX_SENDER_CONTACT_LOOKUP_KEY     = sIndexIncrementer++;


    private static String[] sProjection = {
        ConversationMessageViewColumns._ID,
        ConversationMessageViewColumns.CONVERSATION_ID,
        ConversationMessageViewColumns.PARTICIPANT_ID,

        ConversationMessageViewColumns.PARTS_IDS,
        ConversationMessageViewColumns.PARTS_CONTENT_TYPES,
        ConversationMessageViewColumns.PARTS_CONTENT_URIS,
        ConversationMessageViewColumns.PARTS_WIDTHS,
        ConversationMessageViewColumns.PARTS_HEIGHTS,
        ConversationMessageViewColumns.PARTS_TEXTS,

        ConversationMessageViewColumns.PARTS_COUNT,
        ConversationMessageViewColumns.SENT_TIMESTAMP,
        ConversationMessageViewColumns.RECEIVED_TIMESTAMP,
        ConversationMessageViewColumns.SEEN,
        ConversationMessageViewColumns.READ,
        ConversationMessageViewColumns.PROTOCOL,
        ConversationMessageViewColumns.STATUS,
        ConversationMessageViewColumns.SMS_MESSAGE_URI,
        ConversationMessageViewColumns.SMS_PRIORITY,
        ConversationMessageViewColumns.SMS_MESSAGE_SIZE,
        ConversationMessageViewColumns.MMS_SUBJECT,
        ConversationMessageViewColumns.MMS_EXPIRY,
        ConversationMessageViewColumns.RAW_TELEPHONY_STATUS,
        ConversationMessageViewColumns.SELF_PARTICIPANT_ID,
        ConversationMessageViewColumns.SENDER_FULL_NAME,
        ConversationMessageViewColumns.SENDER_FIRST_NAME,
        ConversationMessageViewColumns.SENDER_DISPLAY_DESTINATION,
        ConversationMessageViewColumns.SENDER_NORMALIZED_DESTINATION,
        ConversationMessageViewColumns.SENDER_PROFILE_PHOTO_URI,
        ConversationMessageViewColumns.SENDER_CONTACT_ID,
        ConversationMessageViewColumns.SENDER_CONTACT_LOOKUP_KEY,
    };

    public static String[] getProjection() {
        return sProjection;
    }
}