ConversationMessageData.java revision d3b009ae55651f1e60950342468e3c37fdeb0796
1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.messaging.datamodel.data;
17
18import android.database.Cursor;
19import android.net.Uri;
20import android.provider.BaseColumns;
21import android.provider.ContactsContract;
22import android.text.TextUtils;
23import android.text.format.DateUtils;
24
25import com.android.messaging.datamodel.DatabaseHelper;
26import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
27import com.android.messaging.datamodel.DatabaseHelper.PartColumns;
28import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
29import com.android.messaging.util.Assert;
30import com.android.messaging.util.BugleGservices;
31import com.android.messaging.util.BugleGservicesKeys;
32import com.android.messaging.util.ContentType;
33import com.android.messaging.util.Dates;
34import com.android.messaging.util.LogUtil;
35import com.google.common.annotations.VisibleForTesting;
36import com.google.common.base.Predicate;
37
38import java.util.ArrayList;
39import java.util.Collections;
40import java.util.LinkedList;
41import java.util.List;
42
43/**
44 * Class representing a message within a conversation sequence. The message parts
45 * are available via the getParts() method.
46 *
47 * TODO: See if we can delegate to MessageData for the logic that this class duplicates
48 * (e.g. getIsMms).
49 */
50public class ConversationMessageData {
51    private static final String TAG = LogUtil.BUGLE_TAG;
52
53    private String mMessageId;
54    private String mConversationId;
55    private String mParticipantId;
56    private int mPartsCount;
57    private List<MessagePartData> mParts;
58    private long mSentTimestamp;
59    private long mReceivedTimestamp;
60    private boolean mSeen;
61    private boolean mRead;
62    private int mProtocol;
63    private int mStatus;
64    private String mSmsMessageUri;
65    private int mSmsPriority;
66    private int mSmsMessageSize;
67    private String mMmsSubject;
68    private long mMmsExpiry;
69    private int mRawTelephonyStatus;
70    private String mSenderFullName;
71    private String mSenderFirstName;
72    private String mSenderDisplayDestination;
73    private String mSenderNormalizedDestination;
74    private String mSenderProfilePhotoUri;
75    private long mSenderContactId;
76    private String mSenderContactLookupKey;
77    private String mSelfParticipantId;
78
79    /** Are we similar enough to the previous/next messages that we can cluster them? */
80    private boolean mCanClusterWithPreviousMessage;
81    private boolean mCanClusterWithNextMessage;
82
83    public ConversationMessageData() {
84    }
85
86    public void bind(final Cursor cursor) {
87        mMessageId = cursor.getString(INDEX_MESSAGE_ID);
88        mConversationId = cursor.getString(INDEX_CONVERSATION_ID);
89        mParticipantId = cursor.getString(INDEX_PARTICIPANT_ID);
90        mPartsCount = cursor.getInt(INDEX_PARTS_COUNT);
91
92        mParts = makeParts(
93                cursor.getString(INDEX_PARTS_IDS),
94                cursor.getString(INDEX_PARTS_CONTENT_TYPES),
95                cursor.getString(INDEX_PARTS_CONTENT_URIS),
96                cursor.getString(INDEX_PARTS_WIDTHS),
97                cursor.getString(INDEX_PARTS_HEIGHTS),
98                cursor.getString(INDEX_PARTS_TEXTS),
99                mPartsCount,
100                mMessageId);
101
102        mSentTimestamp = cursor.getLong(INDEX_SENT_TIMESTAMP);
103        mReceivedTimestamp = cursor.getLong(INDEX_RECEIVED_TIMESTAMP);
104        mSeen = (cursor.getInt(INDEX_SEEN) != 0);
105        mRead = (cursor.getInt(INDEX_READ) != 0);
106        mProtocol = cursor.getInt(INDEX_PROTOCOL);
107        mStatus = cursor.getInt(INDEX_STATUS);
108        mSmsMessageUri = cursor.getString(INDEX_SMS_MESSAGE_URI);
109        mSmsPriority = cursor.getInt(INDEX_SMS_PRIORITY);
110        mSmsMessageSize = cursor.getInt(INDEX_SMS_MESSAGE_SIZE);
111        mMmsSubject = cursor.getString(INDEX_MMS_SUBJECT);
112        mMmsExpiry = cursor.getLong(INDEX_MMS_EXPIRY);
113        mRawTelephonyStatus = cursor.getInt(INDEX_RAW_TELEPHONY_STATUS);
114        mSenderFullName = cursor.getString(INDEX_SENDER_FULL_NAME);
115        mSenderFirstName = cursor.getString(INDEX_SENDER_FIRST_NAME);
116        mSenderDisplayDestination = cursor.getString(INDEX_SENDER_DISPLAY_DESTINATION);
117        mSenderNormalizedDestination = cursor.getString(INDEX_SENDER_NORMALIZED_DESTINATION);
118        mSenderProfilePhotoUri = cursor.getString(INDEX_SENDER_PROFILE_PHOTO_URI);
119        mSenderContactId = cursor.getLong(INDEX_SENDER_CONTACT_ID);
120        mSenderContactLookupKey = cursor.getString(INDEX_SENDER_CONTACT_LOOKUP_KEY);
121        mSelfParticipantId = cursor.getString(INDEX_SELF_PARTICIPIANT_ID);
122
123        if (!cursor.isFirst() && cursor.moveToPrevious()) {
124            mCanClusterWithPreviousMessage = canClusterWithMessage(cursor);
125            cursor.moveToNext();
126        } else {
127            mCanClusterWithPreviousMessage = false;
128        }
129        if (!cursor.isLast() && cursor.moveToNext()) {
130            mCanClusterWithNextMessage = canClusterWithMessage(cursor);
131            cursor.moveToPrevious();
132        } else {
133            mCanClusterWithNextMessage = false;
134        }
135    }
136
137    private boolean canClusterWithMessage(final Cursor cursor) {
138        final String otherParticipantId = cursor.getString(INDEX_PARTICIPANT_ID);
139        if (!TextUtils.equals(getParticipantId(), otherParticipantId)) {
140            return false;
141        }
142        final int otherStatus = cursor.getInt(INDEX_STATUS);
143        final boolean otherIsIncoming = (otherStatus >= MessageData.BUGLE_STATUS_FIRST_INCOMING);
144        if (getIsIncoming() != otherIsIncoming) {
145            return false;
146        }
147        final long otherReceivedTimestamp = cursor.getLong(INDEX_RECEIVED_TIMESTAMP);
148        final long timestampDeltaMillis = Math.abs(mReceivedTimestamp - otherReceivedTimestamp);
149        if (timestampDeltaMillis > DateUtils.MINUTE_IN_MILLIS) {
150            return false;
151        }
152        final String otherSelfId = cursor.getString(INDEX_SELF_PARTICIPIANT_ID);
153        if (!TextUtils.equals(getSelfParticipantId(), otherSelfId)) {
154            return false;
155        }
156        return true;
157    }
158
159    private static final Character QUOTE_CHAR = '\'';
160    private static final char DIVIDER = '|';
161
162    // statics to avoid unnecessary object allocation
163    private static final StringBuilder sUnquoteStringBuilder = new StringBuilder();
164    private static final ArrayList<String> sUnquoteResults = new ArrayList<String>();
165
166    // this lock is used to guard access to the above statics
167    private static final Object sUnquoteLock = new Object();
168
169    private static void addResult(final ArrayList<String> results, final StringBuilder value) {
170        if (value.length() > 0) {
171            results.add(value.toString());
172        } else {
173            results.add(EMPTY_STRING);
174        }
175    }
176
177    @VisibleForTesting
178    static String[] splitUnquotedString(final String inputString) {
179        if (TextUtils.isEmpty(inputString)) {
180            return new String[0];
181        }
182
183        return inputString.split("\\" + DIVIDER);
184    }
185
186    /**
187     * Takes a group-concated and quoted string and decomposes it into its constituent
188     * parts.  A quoted string starts and ends with a single quote.  Actual single quotes
189     * within the string are escaped using a second single quote.  So, for example, an
190     * input string with 3 constituent parts might look like this:
191     *
192     * 'now is the time'|'I can''t do it'|'foo'
193     *
194     * This would be returned as an array of 3 strings as follows:
195     * now is the time
196     * I can't do it
197     * foo
198     *
199     * This is achieved by walking through the inputString, character by character,
200     * ignoring the outer quotes and the divider and replacing any pair of consecutive
201     * single quotes with a single single quote.
202     *
203     * @param inputString
204     * @return array of constituent strings
205     */
206    @VisibleForTesting
207    static String[] splitQuotedString(final String inputString) {
208        if (TextUtils.isEmpty(inputString)) {
209            return new String[0];
210        }
211
212        // this method can be called from multiple threads but it uses a static
213        // string builder
214        synchronized (sUnquoteLock) {
215            final int length = inputString.length();
216            final ArrayList<String> results = sUnquoteResults;
217            results.clear();
218
219            int characterPos = -1;
220            while (++characterPos < length) {
221                final char mustBeQuote = inputString.charAt(characterPos);
222                Assert.isTrue(QUOTE_CHAR == mustBeQuote);
223                while (++characterPos < length) {
224                    final char currentChar = inputString.charAt(characterPos);
225                    if (currentChar == QUOTE_CHAR) {
226                        final char peekAhead = characterPos < length - 1
227                                ? inputString.charAt(characterPos + 1) : 0;
228
229                        if (peekAhead == QUOTE_CHAR) {
230                            characterPos += 1;  // skip the second quote
231                        } else {
232                            addResult(results, sUnquoteStringBuilder);
233                            sUnquoteStringBuilder.setLength(0);
234
235                            Assert.isTrue((peekAhead == DIVIDER) || (peekAhead == (char) 0));
236                            characterPos += 1;  // skip the divider
237                            break;
238                        }
239                    }
240                    sUnquoteStringBuilder.append(currentChar);
241                }
242            }
243            return results.toArray(new String[results.size()]);
244        }
245    }
246
247    static MessagePartData makePartData(
248            final String partId,
249            final String contentType,
250            final String contentUriString,
251            final String contentWidth,
252            final String contentHeight,
253            final String text,
254            final String messageId) {
255        if (ContentType.isTextType(contentType)) {
256            final MessagePartData textPart = MessagePartData.createTextMessagePart(text);
257            textPart.updatePartId(partId);
258            textPart.updateMessageId(messageId);
259            return textPart;
260        } else {
261            final Uri contentUri = Uri.parse(contentUriString);
262            final int width = Integer.parseInt(contentWidth);
263            final int height = Integer.parseInt(contentHeight);
264            final MessagePartData attachmentPart = MessagePartData.createMediaMessagePart(
265                    contentType, contentUri, width, height);
266            attachmentPart.updatePartId(partId);
267            attachmentPart.updateMessageId(messageId);
268            return attachmentPart;
269        }
270    }
271
272    @VisibleForTesting
273    static List<MessagePartData> makeParts(
274            final String rawIds,
275            final String rawContentTypes,
276            final String rawContentUris,
277            final String rawWidths,
278            final String rawHeights,
279            final String rawTexts,
280            final int partsCount,
281            final String messageId) {
282        final List<MessagePartData> parts = new LinkedList<MessagePartData>();
283        if (partsCount == 1) {
284            parts.add(makePartData(
285                    rawIds,
286                    rawContentTypes,
287                    rawContentUris,
288                    rawWidths,
289                    rawHeights,
290                    rawTexts,
291                    messageId));
292        } else {
293            unpackMessageParts(
294                    parts,
295                    splitUnquotedString(rawIds),
296                    splitQuotedString(rawContentTypes),
297                    splitQuotedString(rawContentUris),
298                    splitUnquotedString(rawWidths),
299                    splitUnquotedString(rawHeights),
300                    splitQuotedString(rawTexts),
301                    partsCount,
302                    messageId);
303        }
304        return parts;
305    }
306
307    @VisibleForTesting
308    static void unpackMessageParts(
309            final List<MessagePartData> parts,
310            final String[] ids,
311            final String[] contentTypes,
312            final String[] contentUris,
313            final String[] contentWidths,
314            final String[] contentHeights,
315            final String[] texts,
316            final int partsCount,
317            final String messageId) {
318
319        Assert.equals(partsCount, ids.length);
320        Assert.equals(partsCount, contentTypes.length);
321        Assert.equals(partsCount, contentUris.length);
322        Assert.equals(partsCount, contentWidths.length);
323        Assert.equals(partsCount, contentHeights.length);
324        Assert.equals(partsCount, texts.length);
325
326        for (int i = 0; i < partsCount; i++) {
327            parts.add(makePartData(
328                    ids[i],
329                    contentTypes[i],
330                    contentUris[i],
331                    contentWidths[i],
332                    contentHeights[i],
333                    texts[i],
334                    messageId));
335        }
336
337        if (parts.size() != partsCount) {
338            LogUtil.wtf(TAG, "Only unpacked " + parts.size() + " parts from message (id="
339                    + messageId + "), expected " + partsCount + " parts");
340        }
341    }
342
343    public final String getMessageId() {
344        return mMessageId;
345    }
346
347    public final String getConversationId() {
348        return mConversationId;
349    }
350
351    public final String getParticipantId() {
352        return mParticipantId;
353    }
354
355    public List<MessagePartData> getParts() {
356        return mParts;
357    }
358
359    public boolean hasText() {
360        for (final MessagePartData part : mParts) {
361            if (part.isText()) {
362                return true;
363            }
364        }
365        return false;
366    }
367
368    /**
369     * Get a concatenation of all text parts
370     *
371     * @return the text that is a concatenation of all text parts
372     */
373    public String getText() {
374        // This is optimized for single text part case, which is the majority
375
376        // For single text part, we just return the part without creating the StringBuilder
377        String firstTextPart = null;
378        boolean foundText = false;
379        // For multiple text parts, we need the StringBuilder and the separator for concatenation
380        StringBuilder sb = null;
381        String separator = null;
382        for (final MessagePartData part : mParts) {
383            if (part.isText()) {
384                if (!foundText) {
385                    // First text part
386                    firstTextPart = part.getText();
387                    foundText = true;
388                } else {
389                    // Second and beyond
390                    if (sb == null) {
391                        // Need the StringBuilder and the separator starting from 2nd text part
392                        sb = new StringBuilder();
393                        if (!TextUtils.isEmpty(firstTextPart)) {
394                              sb.append(firstTextPart);
395                        }
396                        separator = BugleGservices.get().getString(
397                                BugleGservicesKeys.MMS_TEXT_CONCAT_SEPARATOR,
398                                BugleGservicesKeys.MMS_TEXT_CONCAT_SEPARATOR_DEFAULT);
399                    }
400                    final String partText = part.getText();
401                    if (!TextUtils.isEmpty(partText)) {
402                        if (!TextUtils.isEmpty(separator) && sb.length() > 0) {
403                            sb.append(separator);
404                        }
405                        sb.append(partText);
406                    }
407                }
408            }
409        }
410        if (sb == null) {
411            // Only one text part
412            return firstTextPart;
413        } else {
414            // More than one
415            return sb.toString();
416        }
417    }
418
419    public boolean hasAttachments() {
420        for (final MessagePartData part : mParts) {
421            if (part.isAttachment()) {
422                return true;
423            }
424        }
425        return false;
426    }
427
428    public List<MessagePartData> getAttachments() {
429        return getAttachments(null);
430    }
431
432    public List<MessagePartData> getAttachments(final Predicate<MessagePartData> filter) {
433        if (mParts.isEmpty()) {
434            return Collections.emptyList();
435        }
436        final List<MessagePartData> attachmentParts = new LinkedList<>();
437        for (final MessagePartData part : mParts) {
438            if (part.isAttachment()) {
439                if (filter == null || filter.apply(part)) {
440                    attachmentParts.add(part);
441                }
442            }
443        }
444        return attachmentParts;
445    }
446
447    public final long getSentTimeStamp() {
448        return mSentTimestamp;
449    }
450
451    public final long getReceivedTimeStamp() {
452        return mReceivedTimestamp;
453    }
454
455    public final String getFormattedReceivedTimeStamp() {
456        return Dates.getMessageTimeString(mReceivedTimestamp).toString();
457    }
458
459    public final boolean getIsSeen() {
460        return mSeen;
461    }
462
463    public final boolean getIsRead() {
464        return mRead;
465    }
466
467    public final boolean getIsMms() {
468        return (mProtocol == MessageData.PROTOCOL_MMS ||
469                mProtocol == MessageData.PROTOCOL_MMS_PUSH_NOTIFICATION);
470    }
471
472    public final boolean getIsMmsNotification() {
473        return (mProtocol == MessageData.PROTOCOL_MMS_PUSH_NOTIFICATION);
474    }
475
476    public final boolean getIsSms() {
477        return mProtocol == (MessageData.PROTOCOL_SMS);
478    }
479
480    final int getProtocol() {
481        return mProtocol;
482    }
483
484    public final int getStatus() {
485        return mStatus;
486    }
487
488    public final String getSmsMessageUri() {
489        return mSmsMessageUri;
490    }
491
492    public final int getSmsPriority() {
493        return mSmsPriority;
494    }
495
496    public final int getSmsMessageSize() {
497        return mSmsMessageSize;
498    }
499
500    public final String getMmsSubject() {
501        return mMmsSubject;
502    }
503
504    public final long getMmsExpiry() {
505        return mMmsExpiry;
506    }
507
508    public final int getRawTelephonyStatus() {
509        return mRawTelephonyStatus;
510    }
511
512    public final String getSelfParticipantId() {
513        return mSelfParticipantId;
514    }
515
516    public boolean getIsIncoming() {
517        return (mStatus >= MessageData.BUGLE_STATUS_FIRST_INCOMING);
518    }
519
520    public boolean hasIncomingErrorStatus() {
521        return (mStatus == MessageData.BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE ||
522                mStatus == MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED);
523    }
524
525    public boolean getIsSendComplete() {
526        return mStatus == MessageData.BUGLE_STATUS_OUTGOING_COMPLETE;
527    }
528
529    public String getSenderFullName() {
530        return mSenderFullName;
531    }
532
533    public String getSenderFirstName() {
534        return mSenderFirstName;
535    }
536
537    public String getSenderDisplayDestination() {
538        return mSenderDisplayDestination;
539    }
540
541    public String getSenderNormalizedDestination() {
542        return mSenderNormalizedDestination;
543    }
544
545    public Uri getSenderProfilePhotoUri() {
546        return mSenderProfilePhotoUri == null ? null : Uri.parse(mSenderProfilePhotoUri);
547    }
548
549    public long getSenderContactId() {
550        return mSenderContactId;
551    }
552
553    public String getSenderDisplayName() {
554        if (!TextUtils.isEmpty(mSenderFullName)) {
555            return mSenderFullName;
556        }
557        if (!TextUtils.isEmpty(mSenderFirstName)) {
558            return mSenderFirstName;
559        }
560        return mSenderDisplayDestination;
561    }
562
563    public String getSenderContactLookupKey() {
564        return mSenderContactLookupKey;
565    }
566
567    public boolean getShowDownloadMessage() {
568        return MessageData.getShowDownloadMessage(mStatus);
569    }
570
571    public boolean getShowResendMessage() {
572        return MessageData.getShowResendMessage(mStatus);
573    }
574
575    public boolean getCanForwardMessage() {
576        // Even for outgoing messages, we only allow forwarding if the message has finished sending
577        // as media often has issues when send isn't complete
578        return (mStatus == MessageData.BUGLE_STATUS_OUTGOING_COMPLETE ||
579                mStatus == MessageData.BUGLE_STATUS_INCOMING_COMPLETE);
580    }
581
582    public boolean getCanCopyMessageToClipboard() {
583        return (hasText() &&
584                (!getIsIncoming() || mStatus == MessageData.BUGLE_STATUS_INCOMING_COMPLETE));
585    }
586
587    public boolean getOneClickResendMessage() {
588        return MessageData.getOneClickResendMessage(mStatus, mRawTelephonyStatus);
589    }
590
591    /**
592     * Get sender's lookup uri.
593     * This method doesn't support corp contacts.
594     *
595     * @return Lookup uri of sender's contact
596     */
597    public Uri getSenderContactLookupUri() {
598        if (mSenderContactId > ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED
599                && !TextUtils.isEmpty(mSenderContactLookupKey)) {
600            return ContactsContract.Contacts.getLookupUri(mSenderContactId,
601                    mSenderContactLookupKey);
602        }
603        return null;
604    }
605
606    public boolean getCanClusterWithPreviousMessage() {
607        return mCanClusterWithPreviousMessage;
608    }
609
610    public boolean getCanClusterWithNextMessage() {
611        return mCanClusterWithNextMessage;
612    }
613
614    @Override
615    public String toString() {
616        return MessageData.toString(mMessageId, mParts);
617    }
618
619    // Data definitions
620
621    public static final String getConversationMessagesQuerySql() {
622        return CONVERSATION_MESSAGES_QUERY_SQL
623                + " AND "
624                // Inject the conversation id
625                + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?)"
626                + CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY;
627    }
628
629    static final String getConversationMessageIdsQuerySql() {
630        return CONVERSATION_MESSAGES_IDS_QUERY_SQL
631                + " AND "
632                // Inject the conversation id
633                + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?)"
634                + CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY;
635    }
636
637    public static final String getNotificationQuerySql() {
638        return CONVERSATION_MESSAGES_QUERY_SQL
639                + " AND "
640                + "(" + DatabaseHelper.MessageColumns.STATUS + " in ("
641                + MessageData.BUGLE_STATUS_INCOMING_COMPLETE + ", "
642                + MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD + ")"
643                + " AND "
644                + DatabaseHelper.MessageColumns.SEEN + " = 0)"
645                + ")"
646                + NOTIFICATION_QUERY_SQL_GROUP_BY;
647    }
648
649    public static final String getWearableQuerySql() {
650        return CONVERSATION_MESSAGES_QUERY_SQL
651                + " AND "
652                + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?"
653                + " AND "
654                + DatabaseHelper.MessageColumns.STATUS + " IN ("
655                + MessageData.BUGLE_STATUS_OUTGOING_DELIVERED + ", "
656                + MessageData.BUGLE_STATUS_OUTGOING_COMPLETE + ", "
657                + MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND + ", "
658                + MessageData.BUGLE_STATUS_OUTGOING_SENDING + ", "
659                + MessageData.BUGLE_STATUS_OUTGOING_RESENDING + ", "
660                + MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY + ", "
661                + MessageData.BUGLE_STATUS_INCOMING_COMPLETE + ", "
662                + MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD + ")"
663                + ")"
664                + NOTIFICATION_QUERY_SQL_GROUP_BY;
665    }
666
667    /*
668     * Generate a sqlite snippet to call the quote function on the columnName argument.
669     * The columnName doesn't strictly have to be a column name (e.g. it could be an
670     * expression).
671     */
672    private static String quote(final String columnName) {
673        return "quote(" + columnName + ")";
674    }
675
676    private static String makeGroupConcatString(final String column) {
677        return "group_concat(" + column + ", '" + DIVIDER + "')";
678    }
679
680    private static String makeIfNullString(final String column) {
681        return "ifnull(" + column + "," + "''" + ")";
682    }
683
684    private static String makePartsTableColumnString(final String column) {
685        return DatabaseHelper.PARTS_TABLE + '.' + column;
686    }
687
688    private static String makeCaseWhenString(final String column,
689                                             final boolean quote,
690                                             final String asColumn) {
691        final String fullColumn = makeIfNullString(makePartsTableColumnString(column));
692        final String groupConcatTerm = quote
693                ? makeGroupConcatString(quote(fullColumn))
694                : makeGroupConcatString(fullColumn);
695        return "CASE WHEN (" + CONVERSATION_MESSAGE_VIEW_PARTS_COUNT + ">1) THEN " + groupConcatTerm
696                + " ELSE " + makePartsTableColumnString(column) + " END AS " + asColumn;
697    }
698
699    private static final String CONVERSATION_MESSAGE_VIEW_PARTS_COUNT =
700            "count(" + DatabaseHelper.PARTS_TABLE + '.' + PartColumns._ID + ")";
701
702    private static final String EMPTY_STRING = "";
703
704    private static final String CONVERSATION_MESSAGES_QUERY_PROJECTION_SQL =
705            DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns._ID
706            + " as " + ConversationMessageViewColumns._ID + ", "
707            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.CONVERSATION_ID
708            + " as " + ConversationMessageViewColumns.CONVERSATION_ID + ", "
709            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SENDER_PARTICIPANT_ID
710            + " as " + ConversationMessageViewColumns.PARTICIPANT_ID + ", "
711
712            + makeCaseWhenString(PartColumns._ID, false,
713                    ConversationMessageViewColumns.PARTS_IDS) + ", "
714            + makeCaseWhenString(PartColumns.CONTENT_TYPE, true,
715                    ConversationMessageViewColumns.PARTS_CONTENT_TYPES) + ", "
716            + makeCaseWhenString(PartColumns.CONTENT_URI, true,
717                    ConversationMessageViewColumns.PARTS_CONTENT_URIS) + ", "
718            + makeCaseWhenString(PartColumns.WIDTH, false,
719                    ConversationMessageViewColumns.PARTS_WIDTHS) + ", "
720            + makeCaseWhenString(PartColumns.HEIGHT, false,
721                    ConversationMessageViewColumns.PARTS_HEIGHTS) + ", "
722            + makeCaseWhenString(PartColumns.TEXT, true,
723                    ConversationMessageViewColumns.PARTS_TEXTS) + ", "
724
725            + CONVERSATION_MESSAGE_VIEW_PARTS_COUNT
726            + " as " + ConversationMessageViewColumns.PARTS_COUNT + ", "
727
728            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SENT_TIMESTAMP
729            + " as " + ConversationMessageViewColumns.SENT_TIMESTAMP + ", "
730            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP
731            + " as " + ConversationMessageViewColumns.RECEIVED_TIMESTAMP + ", "
732            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SEEN
733            + " as " + ConversationMessageViewColumns.SEEN + ", "
734            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.READ
735            + " as " + ConversationMessageViewColumns.READ + ", "
736            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.PROTOCOL
737            + " as " + ConversationMessageViewColumns.PROTOCOL + ", "
738            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.STATUS
739            + " as " + ConversationMessageViewColumns.STATUS + ", "
740            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SMS_MESSAGE_URI
741            + " as " + ConversationMessageViewColumns.SMS_MESSAGE_URI + ", "
742            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SMS_PRIORITY
743            + " as " + ConversationMessageViewColumns.SMS_PRIORITY + ", "
744            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SMS_MESSAGE_SIZE
745            + " as " + ConversationMessageViewColumns.SMS_MESSAGE_SIZE + ", "
746            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.MMS_SUBJECT
747            + " as " + ConversationMessageViewColumns.MMS_SUBJECT + ", "
748            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.MMS_EXPIRY
749            + " as " + ConversationMessageViewColumns.MMS_EXPIRY + ", "
750            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RAW_TELEPHONY_STATUS
751            + " as " + ConversationMessageViewColumns.RAW_TELEPHONY_STATUS + ", "
752            + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SELF_PARTICIPANT_ID
753            + " as " + ConversationMessageViewColumns.SELF_PARTICIPANT_ID + ", "
754            + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.FULL_NAME
755            + " as " + ConversationMessageViewColumns.SENDER_FULL_NAME + ", "
756            + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.FIRST_NAME
757            + " as " + ConversationMessageViewColumns.SENDER_FIRST_NAME + ", "
758            + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.DISPLAY_DESTINATION
759            + " as " + ConversationMessageViewColumns.SENDER_DISPLAY_DESTINATION + ", "
760            + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.NORMALIZED_DESTINATION
761            + " as " + ConversationMessageViewColumns.SENDER_NORMALIZED_DESTINATION + ", "
762            + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.PROFILE_PHOTO_URI
763            + " as " + ConversationMessageViewColumns.SENDER_PROFILE_PHOTO_URI + ", "
764            + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.CONTACT_ID
765            + " as " + ConversationMessageViewColumns.SENDER_CONTACT_ID + ", "
766            + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.LOOKUP_KEY
767            + " as " + ConversationMessageViewColumns.SENDER_CONTACT_LOOKUP_KEY + " ";
768
769    private static final String CONVERSATION_MESSAGES_QUERY_FROM_WHERE_SQL =
770            " FROM " + DatabaseHelper.MESSAGES_TABLE
771            + " LEFT JOIN " + DatabaseHelper.PARTS_TABLE
772            + " ON (" + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns._ID
773            + "=" + DatabaseHelper.PARTS_TABLE + "." + PartColumns.MESSAGE_ID + ") "
774            + " LEFT JOIN " + DatabaseHelper.PARTICIPANTS_TABLE
775            + " ON (" + DatabaseHelper.MESSAGES_TABLE + '.' +  MessageColumns.SENDER_PARTICIPANT_ID
776            + '=' + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns._ID + ")"
777            // Exclude draft messages from main view
778            + " WHERE (" + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.STATUS
779            + " <> " + MessageData.BUGLE_STATUS_OUTGOING_DRAFT;
780
781    // This query is mostly static, except for the injection of conversation id. This is for
782    // performance reasons, to ensure that the query uses indices and does not trigger full scans
783    // of the messages table. See b/17160946 for more details.
784    private static final String CONVERSATION_MESSAGES_QUERY_SQL = "SELECT "
785            + CONVERSATION_MESSAGES_QUERY_PROJECTION_SQL
786            + CONVERSATION_MESSAGES_QUERY_FROM_WHERE_SQL;
787
788    private static final String CONVERSATION_MESSAGE_IDS_PROJECTION_SQL =
789            DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns._ID
790                    + " as " + ConversationMessageViewColumns._ID + " ";
791
792    private static final String CONVERSATION_MESSAGES_IDS_QUERY_SQL = "SELECT "
793            + CONVERSATION_MESSAGE_IDS_PROJECTION_SQL
794            + CONVERSATION_MESSAGES_QUERY_FROM_WHERE_SQL;
795
796    // Note that we sort DESC and ConversationData reverses the cursor.  This is a performance
797    // issue (improvement) for large cursors.
798    private static final String CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY =
799            " GROUP BY " + DatabaseHelper.PARTS_TABLE + '.' + PartColumns.MESSAGE_ID
800          + " ORDER BY "
801          + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP + " DESC";
802
803    private static final String NOTIFICATION_QUERY_SQL_GROUP_BY =
804            " GROUP BY " + DatabaseHelper.PARTS_TABLE + '.' + PartColumns.MESSAGE_ID
805          + " ORDER BY "
806          + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP + " DESC";
807
808    interface ConversationMessageViewColumns extends BaseColumns {
809        static final String _ID = MessageColumns._ID;
810        static final String CONVERSATION_ID = MessageColumns.CONVERSATION_ID;
811        static final String PARTICIPANT_ID = MessageColumns.SENDER_PARTICIPANT_ID;
812        static final String PARTS_COUNT = "parts_count";
813        static final String SENT_TIMESTAMP = MessageColumns.SENT_TIMESTAMP;
814        static final String RECEIVED_TIMESTAMP = MessageColumns.RECEIVED_TIMESTAMP;
815        static final String SEEN = MessageColumns.SEEN;
816        static final String READ = MessageColumns.READ;
817        static final String PROTOCOL = MessageColumns.PROTOCOL;
818        static final String STATUS = MessageColumns.STATUS;
819        static final String SMS_MESSAGE_URI = MessageColumns.SMS_MESSAGE_URI;
820        static final String SMS_PRIORITY = MessageColumns.SMS_PRIORITY;
821        static final String SMS_MESSAGE_SIZE = MessageColumns.SMS_MESSAGE_SIZE;
822        static final String MMS_SUBJECT = MessageColumns.MMS_SUBJECT;
823        static final String MMS_EXPIRY = MessageColumns.MMS_EXPIRY;
824        static final String RAW_TELEPHONY_STATUS = MessageColumns.RAW_TELEPHONY_STATUS;
825        static final String SELF_PARTICIPANT_ID = MessageColumns.SELF_PARTICIPANT_ID;
826        static final String SENDER_FULL_NAME = ParticipantColumns.FULL_NAME;
827        static final String SENDER_FIRST_NAME = ParticipantColumns.FIRST_NAME;
828        static final String SENDER_DISPLAY_DESTINATION = ParticipantColumns.DISPLAY_DESTINATION;
829        static final String SENDER_NORMALIZED_DESTINATION =
830                ParticipantColumns.NORMALIZED_DESTINATION;
831        static final String SENDER_PROFILE_PHOTO_URI = ParticipantColumns.PROFILE_PHOTO_URI;
832        static final String SENDER_CONTACT_ID = ParticipantColumns.CONTACT_ID;
833        static final String SENDER_CONTACT_LOOKUP_KEY = ParticipantColumns.LOOKUP_KEY;
834        static final String PARTS_IDS = "parts_ids";
835        static final String PARTS_CONTENT_TYPES = "parts_content_types";
836        static final String PARTS_CONTENT_URIS = "parts_content_uris";
837        static final String PARTS_WIDTHS = "parts_widths";
838        static final String PARTS_HEIGHTS = "parts_heights";
839        static final String PARTS_TEXTS = "parts_texts";
840    }
841
842    private static int sIndexIncrementer = 0;
843
844    private static final int INDEX_MESSAGE_ID                    = sIndexIncrementer++;
845    private static final int INDEX_CONVERSATION_ID               = sIndexIncrementer++;
846    private static final int INDEX_PARTICIPANT_ID                = sIndexIncrementer++;
847
848    private static final int INDEX_PARTS_IDS                     = sIndexIncrementer++;
849    private static final int INDEX_PARTS_CONTENT_TYPES           = sIndexIncrementer++;
850    private static final int INDEX_PARTS_CONTENT_URIS            = sIndexIncrementer++;
851    private static final int INDEX_PARTS_WIDTHS                  = sIndexIncrementer++;
852    private static final int INDEX_PARTS_HEIGHTS                 = sIndexIncrementer++;
853    private static final int INDEX_PARTS_TEXTS                   = sIndexIncrementer++;
854
855    private static final int INDEX_PARTS_COUNT                   = sIndexIncrementer++;
856
857    private static final int INDEX_SENT_TIMESTAMP                = sIndexIncrementer++;
858    private static final int INDEX_RECEIVED_TIMESTAMP            = sIndexIncrementer++;
859    private static final int INDEX_SEEN                          = sIndexIncrementer++;
860    private static final int INDEX_READ                          = sIndexIncrementer++;
861    private static final int INDEX_PROTOCOL                      = sIndexIncrementer++;
862    private static final int INDEX_STATUS                        = sIndexIncrementer++;
863    private static final int INDEX_SMS_MESSAGE_URI               = sIndexIncrementer++;
864    private static final int INDEX_SMS_PRIORITY                  = sIndexIncrementer++;
865    private static final int INDEX_SMS_MESSAGE_SIZE              = sIndexIncrementer++;
866    private static final int INDEX_MMS_SUBJECT                   = sIndexIncrementer++;
867    private static final int INDEX_MMS_EXPIRY                    = sIndexIncrementer++;
868    private static final int INDEX_RAW_TELEPHONY_STATUS          = sIndexIncrementer++;
869    private static final int INDEX_SELF_PARTICIPIANT_ID          = sIndexIncrementer++;
870    private static final int INDEX_SENDER_FULL_NAME              = sIndexIncrementer++;
871    private static final int INDEX_SENDER_FIRST_NAME             = sIndexIncrementer++;
872    private static final int INDEX_SENDER_DISPLAY_DESTINATION    = sIndexIncrementer++;
873    private static final int INDEX_SENDER_NORMALIZED_DESTINATION = sIndexIncrementer++;
874    private static final int INDEX_SENDER_PROFILE_PHOTO_URI      = sIndexIncrementer++;
875    private static final int INDEX_SENDER_CONTACT_ID             = sIndexIncrementer++;
876    private static final int INDEX_SENDER_CONTACT_LOOKUP_KEY     = sIndexIncrementer++;
877
878
879    private static String[] sProjection = {
880        ConversationMessageViewColumns._ID,
881        ConversationMessageViewColumns.CONVERSATION_ID,
882        ConversationMessageViewColumns.PARTICIPANT_ID,
883
884        ConversationMessageViewColumns.PARTS_IDS,
885        ConversationMessageViewColumns.PARTS_CONTENT_TYPES,
886        ConversationMessageViewColumns.PARTS_CONTENT_URIS,
887        ConversationMessageViewColumns.PARTS_WIDTHS,
888        ConversationMessageViewColumns.PARTS_HEIGHTS,
889        ConversationMessageViewColumns.PARTS_TEXTS,
890
891        ConversationMessageViewColumns.PARTS_COUNT,
892        ConversationMessageViewColumns.SENT_TIMESTAMP,
893        ConversationMessageViewColumns.RECEIVED_TIMESTAMP,
894        ConversationMessageViewColumns.SEEN,
895        ConversationMessageViewColumns.READ,
896        ConversationMessageViewColumns.PROTOCOL,
897        ConversationMessageViewColumns.STATUS,
898        ConversationMessageViewColumns.SMS_MESSAGE_URI,
899        ConversationMessageViewColumns.SMS_PRIORITY,
900        ConversationMessageViewColumns.SMS_MESSAGE_SIZE,
901        ConversationMessageViewColumns.MMS_SUBJECT,
902        ConversationMessageViewColumns.MMS_EXPIRY,
903        ConversationMessageViewColumns.RAW_TELEPHONY_STATUS,
904        ConversationMessageViewColumns.SELF_PARTICIPANT_ID,
905        ConversationMessageViewColumns.SENDER_FULL_NAME,
906        ConversationMessageViewColumns.SENDER_FIRST_NAME,
907        ConversationMessageViewColumns.SENDER_DISPLAY_DESTINATION,
908        ConversationMessageViewColumns.SENDER_NORMALIZED_DESTINATION,
909        ConversationMessageViewColumns.SENDER_PROFILE_PHOTO_URI,
910        ConversationMessageViewColumns.SENDER_CONTACT_ID,
911        ConversationMessageViewColumns.SENDER_CONTACT_LOOKUP_KEY,
912    };
913
914    public static String[] getProjection() {
915        return sProjection;
916    }
917}
918