• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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     }
528 
getSenderFullName()529     public String getSenderFullName() {
530         return mSenderFullName;
531     }
532 
getSenderFirstName()533     public String getSenderFirstName() {
534         return mSenderFirstName;
535     }
536 
getSenderDisplayDestination()537     public String getSenderDisplayDestination() {
538         return mSenderDisplayDestination;
539     }
540 
getSenderNormalizedDestination()541     public String getSenderNormalizedDestination() {
542         return mSenderNormalizedDestination;
543     }
544 
getSenderProfilePhotoUri()545     public Uri getSenderProfilePhotoUri() {
546         return mSenderProfilePhotoUri == null ? null : Uri.parse(mSenderProfilePhotoUri);
547     }
548 
getSenderContactId()549     public long getSenderContactId() {
550         return mSenderContactId;
551     }
552 
getSenderDisplayName()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 
getSenderContactLookupKey()563     public String getSenderContactLookupKey() {
564         return mSenderContactLookupKey;
565     }
566 
getShowDownloadMessage()567     public boolean getShowDownloadMessage() {
568         return MessageData.getShowDownloadMessage(mStatus);
569     }
570 
getShowResendMessage()571     public boolean getShowResendMessage() {
572         return MessageData.getShowResendMessage(mStatus);
573     }
574 
getCanForwardMessage()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 
getCanCopyMessageToClipboard()582     public boolean getCanCopyMessageToClipboard() {
583         return (hasText() &&
584                 (!getIsIncoming() || mStatus == MessageData.BUGLE_STATUS_INCOMING_COMPLETE));
585     }
586 
getOneClickResendMessage()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      */
getSenderContactLookupUri()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 
getCanClusterWithPreviousMessage()606     public boolean getCanClusterWithPreviousMessage() {
607         return mCanClusterWithPreviousMessage;
608     }
609 
getCanClusterWithNextMessage()610     public boolean getCanClusterWithNextMessage() {
611         return mCanClusterWithNextMessage;
612     }
613 
614     @Override
toString()615     public String toString() {
616         return MessageData.toString(mMessageId, mParts);
617     }
618 
619     // Data definitions
620 
getConversationMessagesQuerySql()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 
getConversationMessageIdsQuerySql()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 
getNotificationQuerySql()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 
getWearableQuerySql()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      */
quote(final String columnName)672     private static String quote(final String columnName) {
673         return "quote(" + columnName + ")";
674     }
675 
makeGroupConcatString(final String column)676     private static String makeGroupConcatString(final String column) {
677         return "group_concat(" + column + ", '" + DIVIDER + "')";
678     }
679 
makeIfNullString(final String column)680     private static String makeIfNullString(final String column) {
681         return "ifnull(" + column + "," + "''" + ")";
682     }
683 
makePartsTableColumnString(final String column)684     private static String makePartsTableColumnString(final String column) {
685         return DatabaseHelper.PARTS_TABLE + '.' + column;
686     }
687 
makeCaseWhenString(final String column, final boolean quote, final String asColumn)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 
getProjection()914     public static String[] getProjection() {
915         return sProjection;
916     }
917 }
918