1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.messaging.datamodel.data;
17 
18 import android.database.Cursor;
19 import android.net.Uri;
20 import android.provider.BaseColumns;
21 import android.provider.ContactsContract;
22 import android.text.TextUtils;
23 import android.text.format.DateUtils;
24 
25 import com.android.messaging.datamodel.DatabaseHelper;
26 import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
27 import com.android.messaging.datamodel.DatabaseHelper.PartColumns;
28 import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
29 import com.android.messaging.util.Assert;
30 import com.android.messaging.util.BugleGservices;
31 import com.android.messaging.util.BugleGservicesKeys;
32 import com.android.messaging.util.ContentType;
33 import com.android.messaging.util.Dates;
34 import com.android.messaging.util.LogUtil;
35 import com.google.common.annotations.VisibleForTesting;
36 import com.google.common.base.Predicate;
37 
38 import java.util.ArrayList;
39 import java.util.Collections;
40 import java.util.LinkedList;
41 import 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  */
50 public 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 
ConversationMessageData()83     public ConversationMessageData() {
84     }
85 
bind(final Cursor cursor)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 
canClusterWithMessage(final Cursor cursor)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 
addResult(final ArrayList<String> results, final StringBuilder value)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
splitUnquotedString(final String inputString)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
splitQuotedString(final String inputString)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 
hasIncomingErrorStatus()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 
getIsSendComplete()525     public boolean getIsSendComplete() {
526         return (mStatus == MessageData.BUGLE_STATUS_OUTGOING_COMPLETE
527                 || mStatus == MessageData.BUGLE_STATUS_OUTGOING_DELIVERED);
528     }
529 
getSenderFullName()530     public String getSenderFullName() {
531         return mSenderFullName;
532     }
533 
getSenderFirstName()534     public String getSenderFirstName() {
535         return mSenderFirstName;
536     }
537 
getSenderDisplayDestination()538     public String getSenderDisplayDestination() {
539         return mSenderDisplayDestination;
540     }
541 
getSenderNormalizedDestination()542     public String getSenderNormalizedDestination() {
543         return mSenderNormalizedDestination;
544     }
545 
getSenderProfilePhotoUri()546     public Uri getSenderProfilePhotoUri() {
547         return mSenderProfilePhotoUri == null ? null : Uri.parse(mSenderProfilePhotoUri);
548     }
549 
getSenderContactId()550     public long getSenderContactId() {
551         return mSenderContactId;
552     }
553 
getSenderDisplayName()554     public String getSenderDisplayName() {
555         if (!TextUtils.isEmpty(mSenderFullName)) {
556             return mSenderFullName;
557         }
558         if (!TextUtils.isEmpty(mSenderFirstName)) {
559             return mSenderFirstName;
560         }
561         return mSenderDisplayDestination;
562     }
563 
getSenderContactLookupKey()564     public String getSenderContactLookupKey() {
565         return mSenderContactLookupKey;
566     }
567 
getShowDownloadMessage()568     public boolean getShowDownloadMessage() {
569         return MessageData.getShowDownloadMessage(mStatus);
570     }
571 
getShowResendMessage()572     public boolean getShowResendMessage() {
573         return MessageData.getShowResendMessage(mStatus);
574     }
575 
getCanForwardMessage()576     public boolean getCanForwardMessage() {
577         // Even for outgoing messages, we only allow forwarding if the message has finished sending
578         // as media often has issues when send isn't complete
579         return (mStatus == MessageData.BUGLE_STATUS_OUTGOING_COMPLETE
580                 || mStatus == MessageData.BUGLE_STATUS_OUTGOING_DELIVERED
581                 || mStatus == MessageData.BUGLE_STATUS_INCOMING_COMPLETE);
582     }
583 
getCanCopyMessageToClipboard()584     public boolean getCanCopyMessageToClipboard() {
585         return (hasText() &&
586                 (!getIsIncoming() || mStatus == MessageData.BUGLE_STATUS_INCOMING_COMPLETE));
587     }
588 
getOneClickResendMessage()589     public boolean getOneClickResendMessage() {
590         return MessageData.getOneClickResendMessage(mStatus, mRawTelephonyStatus);
591     }
592 
593     /**
594      * Get sender's lookup uri.
595      * This method doesn't support corp contacts.
596      *
597      * @return Lookup uri of sender's contact
598      */
getSenderContactLookupUri()599     public Uri getSenderContactLookupUri() {
600         if (mSenderContactId > ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED
601                 && !TextUtils.isEmpty(mSenderContactLookupKey)) {
602             return ContactsContract.Contacts.getLookupUri(mSenderContactId,
603                     mSenderContactLookupKey);
604         }
605         return null;
606     }
607 
getCanClusterWithPreviousMessage()608     public boolean getCanClusterWithPreviousMessage() {
609         return mCanClusterWithPreviousMessage;
610     }
611 
getCanClusterWithNextMessage()612     public boolean getCanClusterWithNextMessage() {
613         return mCanClusterWithNextMessage;
614     }
615 
616     @Override
toString()617     public String toString() {
618         return MessageData.toString(mMessageId, mParts);
619     }
620 
621     // Data definitions
622 
getConversationMessagesQuerySql()623     public static final String getConversationMessagesQuerySql() {
624         return CONVERSATION_MESSAGES_QUERY_SQL
625                 + " AND "
626                 // Inject the conversation id
627                 + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?)"
628                 + CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY;
629     }
630 
getConversationMessageIdsQuerySql()631     static final String getConversationMessageIdsQuerySql() {
632         return CONVERSATION_MESSAGES_IDS_QUERY_SQL
633                 + " AND "
634                 // Inject the conversation id
635                 + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?)"
636                 + CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY;
637     }
638 
getNotificationQuerySql()639     public static final String getNotificationQuerySql() {
640         return CONVERSATION_MESSAGES_QUERY_SQL
641                 + " AND "
642                 + "(" + DatabaseHelper.MessageColumns.STATUS + " in ("
643                 + MessageData.BUGLE_STATUS_INCOMING_COMPLETE + ", "
644                 + MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD + ")"
645                 + " AND "
646                 + DatabaseHelper.MessageColumns.SEEN + " = 0)"
647                 + ")"
648                 + NOTIFICATION_QUERY_SQL_GROUP_BY;
649     }
650 
getWearableQuerySql()651     public static final String getWearableQuerySql() {
652         return CONVERSATION_MESSAGES_QUERY_SQL
653                 + " AND "
654                 + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?"
655                 + " AND "
656                 + DatabaseHelper.MessageColumns.STATUS + " IN ("
657                 + MessageData.BUGLE_STATUS_OUTGOING_DELIVERED + ", "
658                 + MessageData.BUGLE_STATUS_OUTGOING_COMPLETE + ", "
659                 + MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND + ", "
660                 + MessageData.BUGLE_STATUS_OUTGOING_SENDING + ", "
661                 + MessageData.BUGLE_STATUS_OUTGOING_RESENDING + ", "
662                 + MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY + ", "
663                 + MessageData.BUGLE_STATUS_INCOMING_COMPLETE + ", "
664                 + MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD + ")"
665                 + ")"
666                 + NOTIFICATION_QUERY_SQL_GROUP_BY;
667     }
668 
669     /*
670      * Generate a sqlite snippet to call the quote function on the columnName argument.
671      * The columnName doesn't strictly have to be a column name (e.g. it could be an
672      * expression).
673      */
quote(final String columnName)674     private static String quote(final String columnName) {
675         return "quote(" + columnName + ")";
676     }
677 
makeGroupConcatString(final String column)678     private static String makeGroupConcatString(final String column) {
679         return "group_concat(" + column + ", '" + DIVIDER + "')";
680     }
681 
makeIfNullString(final String column)682     private static String makeIfNullString(final String column) {
683         return "ifnull(" + column + "," + "''" + ")";
684     }
685 
makePartsTableColumnString(final String column)686     private static String makePartsTableColumnString(final String column) {
687         return DatabaseHelper.PARTS_TABLE + '.' + column;
688     }
689 
makeCaseWhenString(final String column, final boolean quote, final String asColumn)690     private static String makeCaseWhenString(final String column,
691                                              final boolean quote,
692                                              final String asColumn) {
693         final String fullColumn = makeIfNullString(makePartsTableColumnString(column));
694         final String groupConcatTerm = quote
695                 ? makeGroupConcatString(quote(fullColumn))
696                 : makeGroupConcatString(fullColumn);
697         return "CASE WHEN (" + CONVERSATION_MESSAGE_VIEW_PARTS_COUNT + ">1) THEN " + groupConcatTerm
698                 + " ELSE " + makePartsTableColumnString(column) + " END AS " + asColumn;
699     }
700 
701     private static final String CONVERSATION_MESSAGE_VIEW_PARTS_COUNT =
702             "count(" + DatabaseHelper.PARTS_TABLE + '.' + PartColumns._ID + ")";
703 
704     private static final String EMPTY_STRING = "";
705 
706     private static final String CONVERSATION_MESSAGES_QUERY_PROJECTION_SQL =
707             DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns._ID
708             + " as " + ConversationMessageViewColumns._ID + ", "
709             + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.CONVERSATION_ID
710             + " as " + ConversationMessageViewColumns.CONVERSATION_ID + ", "
711             + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SENDER_PARTICIPANT_ID
712             + " as " + ConversationMessageViewColumns.PARTICIPANT_ID + ", "
713 
714             + makeCaseWhenString(PartColumns._ID, false,
715                     ConversationMessageViewColumns.PARTS_IDS) + ", "
716             + makeCaseWhenString(PartColumns.CONTENT_TYPE, true,
717                     ConversationMessageViewColumns.PARTS_CONTENT_TYPES) + ", "
718             + makeCaseWhenString(PartColumns.CONTENT_URI, true,
719                     ConversationMessageViewColumns.PARTS_CONTENT_URIS) + ", "
720             + makeCaseWhenString(PartColumns.WIDTH, false,
721                     ConversationMessageViewColumns.PARTS_WIDTHS) + ", "
722             + makeCaseWhenString(PartColumns.HEIGHT, false,
723                     ConversationMessageViewColumns.PARTS_HEIGHTS) + ", "
724             + makeCaseWhenString(PartColumns.TEXT, true,
725                     ConversationMessageViewColumns.PARTS_TEXTS) + ", "
726 
727             + CONVERSATION_MESSAGE_VIEW_PARTS_COUNT
728             + " as " + ConversationMessageViewColumns.PARTS_COUNT + ", "
729 
730             + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SENT_TIMESTAMP
731             + " as " + ConversationMessageViewColumns.SENT_TIMESTAMP + ", "
732             + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP
733             + " as " + ConversationMessageViewColumns.RECEIVED_TIMESTAMP + ", "
734             + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SEEN
735             + " as " + ConversationMessageViewColumns.SEEN + ", "
736             + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.READ
737             + " as " + ConversationMessageViewColumns.READ + ", "
738             + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.PROTOCOL
739             + " as " + ConversationMessageViewColumns.PROTOCOL + ", "
740             + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.STATUS
741             + " as " + ConversationMessageViewColumns.STATUS + ", "
742             + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SMS_MESSAGE_URI
743             + " as " + ConversationMessageViewColumns.SMS_MESSAGE_URI + ", "
744             + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SMS_PRIORITY
745             + " as " + ConversationMessageViewColumns.SMS_PRIORITY + ", "
746             + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SMS_MESSAGE_SIZE
747             + " as " + ConversationMessageViewColumns.SMS_MESSAGE_SIZE + ", "
748             + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.MMS_SUBJECT
749             + " as " + ConversationMessageViewColumns.MMS_SUBJECT + ", "
750             + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.MMS_EXPIRY
751             + " as " + ConversationMessageViewColumns.MMS_EXPIRY + ", "
752             + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RAW_TELEPHONY_STATUS
753             + " as " + ConversationMessageViewColumns.RAW_TELEPHONY_STATUS + ", "
754             + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SELF_PARTICIPANT_ID
755             + " as " + ConversationMessageViewColumns.SELF_PARTICIPANT_ID + ", "
756             + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.FULL_NAME
757             + " as " + ConversationMessageViewColumns.SENDER_FULL_NAME + ", "
758             + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.FIRST_NAME
759             + " as " + ConversationMessageViewColumns.SENDER_FIRST_NAME + ", "
760             + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.DISPLAY_DESTINATION
761             + " as " + ConversationMessageViewColumns.SENDER_DISPLAY_DESTINATION + ", "
762             + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.NORMALIZED_DESTINATION
763             + " as " + ConversationMessageViewColumns.SENDER_NORMALIZED_DESTINATION + ", "
764             + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.PROFILE_PHOTO_URI
765             + " as " + ConversationMessageViewColumns.SENDER_PROFILE_PHOTO_URI + ", "
766             + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.CONTACT_ID
767             + " as " + ConversationMessageViewColumns.SENDER_CONTACT_ID + ", "
768             + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.LOOKUP_KEY
769             + " as " + ConversationMessageViewColumns.SENDER_CONTACT_LOOKUP_KEY + " ";
770 
771     private static final String CONVERSATION_MESSAGES_QUERY_FROM_WHERE_SQL =
772             " FROM " + DatabaseHelper.MESSAGES_TABLE
773             + " LEFT JOIN " + DatabaseHelper.PARTS_TABLE
774             + " ON (" + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns._ID
775             + "=" + DatabaseHelper.PARTS_TABLE + "." + PartColumns.MESSAGE_ID + ") "
776             + " LEFT JOIN " + DatabaseHelper.PARTICIPANTS_TABLE
777             + " ON (" + DatabaseHelper.MESSAGES_TABLE + '.' +  MessageColumns.SENDER_PARTICIPANT_ID
778             + '=' + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns._ID + ")"
779             // Exclude draft messages from main view
780             + " WHERE (" + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.STATUS
781             + " <> " + MessageData.BUGLE_STATUS_OUTGOING_DRAFT;
782 
783     // This query is mostly static, except for the injection of conversation id. This is for
784     // performance reasons, to ensure that the query uses indices and does not trigger full scans
785     // of the messages table. See b/17160946 for more details.
786     private static final String CONVERSATION_MESSAGES_QUERY_SQL = "SELECT "
787             + CONVERSATION_MESSAGES_QUERY_PROJECTION_SQL
788             + CONVERSATION_MESSAGES_QUERY_FROM_WHERE_SQL;
789 
790     private static final String CONVERSATION_MESSAGE_IDS_PROJECTION_SQL =
791             DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns._ID
792                     + " as " + ConversationMessageViewColumns._ID + " ";
793 
794     private static final String CONVERSATION_MESSAGES_IDS_QUERY_SQL = "SELECT "
795             + CONVERSATION_MESSAGE_IDS_PROJECTION_SQL
796             + CONVERSATION_MESSAGES_QUERY_FROM_WHERE_SQL;
797 
798     // Note that we sort DESC and ConversationData reverses the cursor.  This is a performance
799     // issue (improvement) for large cursors.
800     private static final String CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY =
801             " GROUP BY " + DatabaseHelper.PARTS_TABLE + '.' + PartColumns.MESSAGE_ID
802           + " ORDER BY "
803           + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP + " DESC";
804 
805     private static final String NOTIFICATION_QUERY_SQL_GROUP_BY =
806             " GROUP BY " + DatabaseHelper.PARTS_TABLE + '.' + PartColumns.MESSAGE_ID
807           + " ORDER BY "
808           + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP + " DESC";
809 
810     interface ConversationMessageViewColumns extends BaseColumns {
811         static final String _ID = MessageColumns._ID;
812         static final String CONVERSATION_ID = MessageColumns.CONVERSATION_ID;
813         static final String PARTICIPANT_ID = MessageColumns.SENDER_PARTICIPANT_ID;
814         static final String PARTS_COUNT = "parts_count";
815         static final String SENT_TIMESTAMP = MessageColumns.SENT_TIMESTAMP;
816         static final String RECEIVED_TIMESTAMP = MessageColumns.RECEIVED_TIMESTAMP;
817         static final String SEEN = MessageColumns.SEEN;
818         static final String READ = MessageColumns.READ;
819         static final String PROTOCOL = MessageColumns.PROTOCOL;
820         static final String STATUS = MessageColumns.STATUS;
821         static final String SMS_MESSAGE_URI = MessageColumns.SMS_MESSAGE_URI;
822         static final String SMS_PRIORITY = MessageColumns.SMS_PRIORITY;
823         static final String SMS_MESSAGE_SIZE = MessageColumns.SMS_MESSAGE_SIZE;
824         static final String MMS_SUBJECT = MessageColumns.MMS_SUBJECT;
825         static final String MMS_EXPIRY = MessageColumns.MMS_EXPIRY;
826         static final String RAW_TELEPHONY_STATUS = MessageColumns.RAW_TELEPHONY_STATUS;
827         static final String SELF_PARTICIPANT_ID = MessageColumns.SELF_PARTICIPANT_ID;
828         static final String SENDER_FULL_NAME = ParticipantColumns.FULL_NAME;
829         static final String SENDER_FIRST_NAME = ParticipantColumns.FIRST_NAME;
830         static final String SENDER_DISPLAY_DESTINATION = ParticipantColumns.DISPLAY_DESTINATION;
831         static final String SENDER_NORMALIZED_DESTINATION =
832                 ParticipantColumns.NORMALIZED_DESTINATION;
833         static final String SENDER_PROFILE_PHOTO_URI = ParticipantColumns.PROFILE_PHOTO_URI;
834         static final String SENDER_CONTACT_ID = ParticipantColumns.CONTACT_ID;
835         static final String SENDER_CONTACT_LOOKUP_KEY = ParticipantColumns.LOOKUP_KEY;
836         static final String PARTS_IDS = "parts_ids";
837         static final String PARTS_CONTENT_TYPES = "parts_content_types";
838         static final String PARTS_CONTENT_URIS = "parts_content_uris";
839         static final String PARTS_WIDTHS = "parts_widths";
840         static final String PARTS_HEIGHTS = "parts_heights";
841         static final String PARTS_TEXTS = "parts_texts";
842     }
843 
844     private static int sIndexIncrementer = 0;
845 
846     private static final int INDEX_MESSAGE_ID                    = sIndexIncrementer++;
847     private static final int INDEX_CONVERSATION_ID               = sIndexIncrementer++;
848     private static final int INDEX_PARTICIPANT_ID                = sIndexIncrementer++;
849 
850     private static final int INDEX_PARTS_IDS                     = sIndexIncrementer++;
851     private static final int INDEX_PARTS_CONTENT_TYPES           = sIndexIncrementer++;
852     private static final int INDEX_PARTS_CONTENT_URIS            = sIndexIncrementer++;
853     private static final int INDEX_PARTS_WIDTHS                  = sIndexIncrementer++;
854     private static final int INDEX_PARTS_HEIGHTS                 = sIndexIncrementer++;
855     private static final int INDEX_PARTS_TEXTS                   = sIndexIncrementer++;
856 
857     private static final int INDEX_PARTS_COUNT                   = sIndexIncrementer++;
858 
859     private static final int INDEX_SENT_TIMESTAMP                = sIndexIncrementer++;
860     private static final int INDEX_RECEIVED_TIMESTAMP            = sIndexIncrementer++;
861     private static final int INDEX_SEEN                          = sIndexIncrementer++;
862     private static final int INDEX_READ                          = sIndexIncrementer++;
863     private static final int INDEX_PROTOCOL                      = sIndexIncrementer++;
864     private static final int INDEX_STATUS                        = sIndexIncrementer++;
865     private static final int INDEX_SMS_MESSAGE_URI               = sIndexIncrementer++;
866     private static final int INDEX_SMS_PRIORITY                  = sIndexIncrementer++;
867     private static final int INDEX_SMS_MESSAGE_SIZE              = sIndexIncrementer++;
868     private static final int INDEX_MMS_SUBJECT                   = sIndexIncrementer++;
869     private static final int INDEX_MMS_EXPIRY                    = sIndexIncrementer++;
870     private static final int INDEX_RAW_TELEPHONY_STATUS          = sIndexIncrementer++;
871     private static final int INDEX_SELF_PARTICIPIANT_ID          = sIndexIncrementer++;
872     private static final int INDEX_SENDER_FULL_NAME              = sIndexIncrementer++;
873     private static final int INDEX_SENDER_FIRST_NAME             = sIndexIncrementer++;
874     private static final int INDEX_SENDER_DISPLAY_DESTINATION    = sIndexIncrementer++;
875     private static final int INDEX_SENDER_NORMALIZED_DESTINATION = sIndexIncrementer++;
876     private static final int INDEX_SENDER_PROFILE_PHOTO_URI      = sIndexIncrementer++;
877     private static final int INDEX_SENDER_CONTACT_ID             = sIndexIncrementer++;
878     private static final int INDEX_SENDER_CONTACT_LOOKUP_KEY     = sIndexIncrementer++;
879 
880 
881     private static String[] sProjection = {
882         ConversationMessageViewColumns._ID,
883         ConversationMessageViewColumns.CONVERSATION_ID,
884         ConversationMessageViewColumns.PARTICIPANT_ID,
885 
886         ConversationMessageViewColumns.PARTS_IDS,
887         ConversationMessageViewColumns.PARTS_CONTENT_TYPES,
888         ConversationMessageViewColumns.PARTS_CONTENT_URIS,
889         ConversationMessageViewColumns.PARTS_WIDTHS,
890         ConversationMessageViewColumns.PARTS_HEIGHTS,
891         ConversationMessageViewColumns.PARTS_TEXTS,
892 
893         ConversationMessageViewColumns.PARTS_COUNT,
894         ConversationMessageViewColumns.SENT_TIMESTAMP,
895         ConversationMessageViewColumns.RECEIVED_TIMESTAMP,
896         ConversationMessageViewColumns.SEEN,
897         ConversationMessageViewColumns.READ,
898         ConversationMessageViewColumns.PROTOCOL,
899         ConversationMessageViewColumns.STATUS,
900         ConversationMessageViewColumns.SMS_MESSAGE_URI,
901         ConversationMessageViewColumns.SMS_PRIORITY,
902         ConversationMessageViewColumns.SMS_MESSAGE_SIZE,
903         ConversationMessageViewColumns.MMS_SUBJECT,
904         ConversationMessageViewColumns.MMS_EXPIRY,
905         ConversationMessageViewColumns.RAW_TELEPHONY_STATUS,
906         ConversationMessageViewColumns.SELF_PARTICIPANT_ID,
907         ConversationMessageViewColumns.SENDER_FULL_NAME,
908         ConversationMessageViewColumns.SENDER_FIRST_NAME,
909         ConversationMessageViewColumns.SENDER_DISPLAY_DESTINATION,
910         ConversationMessageViewColumns.SENDER_NORMALIZED_DESTINATION,
911         ConversationMessageViewColumns.SENDER_PROFILE_PHOTO_URI,
912         ConversationMessageViewColumns.SENDER_CONTACT_ID,
913         ConversationMessageViewColumns.SENDER_CONTACT_LOOKUP_KEY,
914     };
915 
getProjection()916     public static String[] getProjection() {
917         return sProjection;
918     }
919 }
920