Home | History | Annotate | Download | only in data
      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 
     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