/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.messaging.datamodel.data; import android.database.Cursor; import android.net.Uri; import android.provider.BaseColumns; import android.provider.ContactsContract; import android.text.TextUtils; import android.text.format.DateUtils; import com.android.messaging.datamodel.DatabaseHelper; import com.android.messaging.datamodel.DatabaseHelper.MessageColumns; import com.android.messaging.datamodel.DatabaseHelper.PartColumns; import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns; import com.android.messaging.util.Assert; import com.android.messaging.util.BugleGservices; import com.android.messaging.util.BugleGservicesKeys; import com.android.messaging.util.ContentType; import com.android.messaging.util.Dates; import com.android.messaging.util.LogUtil; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Predicate; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; /** * Class representing a message within a conversation sequence. The message parts * are available via the getParts() method. * * TODO: See if we can delegate to MessageData for the logic that this class duplicates * (e.g. getIsMms). */ public class ConversationMessageData { private static final String TAG = LogUtil.BUGLE_TAG; private String mMessageId; private String mConversationId; private String mParticipantId; private int mPartsCount; private List mParts; private long mSentTimestamp; private long mReceivedTimestamp; private boolean mSeen; private boolean mRead; private int mProtocol; private int mStatus; private String mSmsMessageUri; private int mSmsPriority; private int mSmsMessageSize; private String mMmsSubject; private long mMmsExpiry; private int mRawTelephonyStatus; private String mSenderFullName; private String mSenderFirstName; private String mSenderDisplayDestination; private String mSenderNormalizedDestination; private String mSenderProfilePhotoUri; private long mSenderContactId; private String mSenderContactLookupKey; private String mSelfParticipantId; /** Are we similar enough to the previous/next messages that we can cluster them? */ private boolean mCanClusterWithPreviousMessage; private boolean mCanClusterWithNextMessage; public ConversationMessageData() { } public void bind(final Cursor cursor) { mMessageId = cursor.getString(INDEX_MESSAGE_ID); mConversationId = cursor.getString(INDEX_CONVERSATION_ID); mParticipantId = cursor.getString(INDEX_PARTICIPANT_ID); mPartsCount = cursor.getInt(INDEX_PARTS_COUNT); mParts = makeParts( cursor.getString(INDEX_PARTS_IDS), cursor.getString(INDEX_PARTS_CONTENT_TYPES), cursor.getString(INDEX_PARTS_CONTENT_URIS), cursor.getString(INDEX_PARTS_WIDTHS), cursor.getString(INDEX_PARTS_HEIGHTS), cursor.getString(INDEX_PARTS_TEXTS), mPartsCount, mMessageId); mSentTimestamp = cursor.getLong(INDEX_SENT_TIMESTAMP); mReceivedTimestamp = cursor.getLong(INDEX_RECEIVED_TIMESTAMP); mSeen = (cursor.getInt(INDEX_SEEN) != 0); mRead = (cursor.getInt(INDEX_READ) != 0); mProtocol = cursor.getInt(INDEX_PROTOCOL); mStatus = cursor.getInt(INDEX_STATUS); mSmsMessageUri = cursor.getString(INDEX_SMS_MESSAGE_URI); mSmsPriority = cursor.getInt(INDEX_SMS_PRIORITY); mSmsMessageSize = cursor.getInt(INDEX_SMS_MESSAGE_SIZE); mMmsSubject = cursor.getString(INDEX_MMS_SUBJECT); mMmsExpiry = cursor.getLong(INDEX_MMS_EXPIRY); mRawTelephonyStatus = cursor.getInt(INDEX_RAW_TELEPHONY_STATUS); mSenderFullName = cursor.getString(INDEX_SENDER_FULL_NAME); mSenderFirstName = cursor.getString(INDEX_SENDER_FIRST_NAME); mSenderDisplayDestination = cursor.getString(INDEX_SENDER_DISPLAY_DESTINATION); mSenderNormalizedDestination = cursor.getString(INDEX_SENDER_NORMALIZED_DESTINATION); mSenderProfilePhotoUri = cursor.getString(INDEX_SENDER_PROFILE_PHOTO_URI); mSenderContactId = cursor.getLong(INDEX_SENDER_CONTACT_ID); mSenderContactLookupKey = cursor.getString(INDEX_SENDER_CONTACT_LOOKUP_KEY); mSelfParticipantId = cursor.getString(INDEX_SELF_PARTICIPIANT_ID); if (!cursor.isFirst() && cursor.moveToPrevious()) { mCanClusterWithPreviousMessage = canClusterWithMessage(cursor); cursor.moveToNext(); } else { mCanClusterWithPreviousMessage = false; } if (!cursor.isLast() && cursor.moveToNext()) { mCanClusterWithNextMessage = canClusterWithMessage(cursor); cursor.moveToPrevious(); } else { mCanClusterWithNextMessage = false; } } private boolean canClusterWithMessage(final Cursor cursor) { final String otherParticipantId = cursor.getString(INDEX_PARTICIPANT_ID); if (!TextUtils.equals(getParticipantId(), otherParticipantId)) { return false; } final int otherStatus = cursor.getInt(INDEX_STATUS); final boolean otherIsIncoming = (otherStatus >= MessageData.BUGLE_STATUS_FIRST_INCOMING); if (getIsIncoming() != otherIsIncoming) { return false; } final long otherReceivedTimestamp = cursor.getLong(INDEX_RECEIVED_TIMESTAMP); final long timestampDeltaMillis = Math.abs(mReceivedTimestamp - otherReceivedTimestamp); if (timestampDeltaMillis > DateUtils.MINUTE_IN_MILLIS) { return false; } final String otherSelfId = cursor.getString(INDEX_SELF_PARTICIPIANT_ID); if (!TextUtils.equals(getSelfParticipantId(), otherSelfId)) { return false; } return true; } private static final Character QUOTE_CHAR = '\''; private static final char DIVIDER = '|'; // statics to avoid unnecessary object allocation private static final StringBuilder sUnquoteStringBuilder = new StringBuilder(); private static final ArrayList sUnquoteResults = new ArrayList(); // this lock is used to guard access to the above statics private static final Object sUnquoteLock = new Object(); private static void addResult(final ArrayList results, final StringBuilder value) { if (value.length() > 0) { results.add(value.toString()); } else { results.add(EMPTY_STRING); } } @VisibleForTesting static String[] splitUnquotedString(final String inputString) { if (TextUtils.isEmpty(inputString)) { return new String[0]; } return inputString.split("\\" + DIVIDER); } /** * Takes a group-concated and quoted string and decomposes it into its constituent * parts. A quoted string starts and ends with a single quote. Actual single quotes * within the string are escaped using a second single quote. So, for example, an * input string with 3 constituent parts might look like this: * * 'now is the time'|'I can''t do it'|'foo' * * This would be returned as an array of 3 strings as follows: * now is the time * I can't do it * foo * * This is achieved by walking through the inputString, character by character, * ignoring the outer quotes and the divider and replacing any pair of consecutive * single quotes with a single single quote. * * @param inputString * @return array of constituent strings */ @VisibleForTesting static String[] splitQuotedString(final String inputString) { if (TextUtils.isEmpty(inputString)) { return new String[0]; } // this method can be called from multiple threads but it uses a static // string builder synchronized (sUnquoteLock) { final int length = inputString.length(); final ArrayList results = sUnquoteResults; results.clear(); int characterPos = -1; while (++characterPos < length) { final char mustBeQuote = inputString.charAt(characterPos); Assert.isTrue(QUOTE_CHAR == mustBeQuote); while (++characterPos < length) { final char currentChar = inputString.charAt(characterPos); if (currentChar == QUOTE_CHAR) { final char peekAhead = characterPos < length - 1 ? inputString.charAt(characterPos + 1) : 0; if (peekAhead == QUOTE_CHAR) { characterPos += 1; // skip the second quote } else { addResult(results, sUnquoteStringBuilder); sUnquoteStringBuilder.setLength(0); Assert.isTrue((peekAhead == DIVIDER) || (peekAhead == (char) 0)); characterPos += 1; // skip the divider break; } } sUnquoteStringBuilder.append(currentChar); } } return results.toArray(new String[results.size()]); } } static MessagePartData makePartData( final String partId, final String contentType, final String contentUriString, final String contentWidth, final String contentHeight, final String text, final String messageId) { if (ContentType.isTextType(contentType)) { final MessagePartData textPart = MessagePartData.createTextMessagePart(text); textPart.updatePartId(partId); textPart.updateMessageId(messageId); return textPart; } else { final Uri contentUri = Uri.parse(contentUriString); final int width = Integer.parseInt(contentWidth); final int height = Integer.parseInt(contentHeight); final MessagePartData attachmentPart = MessagePartData.createMediaMessagePart( contentType, contentUri, width, height); attachmentPart.updatePartId(partId); attachmentPart.updateMessageId(messageId); return attachmentPart; } } @VisibleForTesting static List makeParts( final String rawIds, final String rawContentTypes, final String rawContentUris, final String rawWidths, final String rawHeights, final String rawTexts, final int partsCount, final String messageId) { final List parts = new LinkedList(); if (partsCount == 1) { parts.add(makePartData( rawIds, rawContentTypes, rawContentUris, rawWidths, rawHeights, rawTexts, messageId)); } else { unpackMessageParts( parts, splitUnquotedString(rawIds), splitQuotedString(rawContentTypes), splitQuotedString(rawContentUris), splitUnquotedString(rawWidths), splitUnquotedString(rawHeights), splitQuotedString(rawTexts), partsCount, messageId); } return parts; } @VisibleForTesting static void unpackMessageParts( final List parts, final String[] ids, final String[] contentTypes, final String[] contentUris, final String[] contentWidths, final String[] contentHeights, final String[] texts, final int partsCount, final String messageId) { Assert.equals(partsCount, ids.length); Assert.equals(partsCount, contentTypes.length); Assert.equals(partsCount, contentUris.length); Assert.equals(partsCount, contentWidths.length); Assert.equals(partsCount, contentHeights.length); Assert.equals(partsCount, texts.length); for (int i = 0; i < partsCount; i++) { parts.add(makePartData( ids[i], contentTypes[i], contentUris[i], contentWidths[i], contentHeights[i], texts[i], messageId)); } if (parts.size() != partsCount) { LogUtil.wtf(TAG, "Only unpacked " + parts.size() + " parts from message (id=" + messageId + "), expected " + partsCount + " parts"); } } public final String getMessageId() { return mMessageId; } public final String getConversationId() { return mConversationId; } public final String getParticipantId() { return mParticipantId; } public List getParts() { return mParts; } public boolean hasText() { for (final MessagePartData part : mParts) { if (part.isText()) { return true; } } return false; } /** * Get a concatenation of all text parts * * @return the text that is a concatenation of all text parts */ public String getText() { // This is optimized for single text part case, which is the majority // For single text part, we just return the part without creating the StringBuilder String firstTextPart = null; boolean foundText = false; // For multiple text parts, we need the StringBuilder and the separator for concatenation StringBuilder sb = null; String separator = null; for (final MessagePartData part : mParts) { if (part.isText()) { if (!foundText) { // First text part firstTextPart = part.getText(); foundText = true; } else { // Second and beyond if (sb == null) { // Need the StringBuilder and the separator starting from 2nd text part sb = new StringBuilder(); if (!TextUtils.isEmpty(firstTextPart)) { sb.append(firstTextPart); } separator = BugleGservices.get().getString( BugleGservicesKeys.MMS_TEXT_CONCAT_SEPARATOR, BugleGservicesKeys.MMS_TEXT_CONCAT_SEPARATOR_DEFAULT); } final String partText = part.getText(); if (!TextUtils.isEmpty(partText)) { if (!TextUtils.isEmpty(separator) && sb.length() > 0) { sb.append(separator); } sb.append(partText); } } } } if (sb == null) { // Only one text part return firstTextPart; } else { // More than one return sb.toString(); } } public boolean hasAttachments() { for (final MessagePartData part : mParts) { if (part.isAttachment()) { return true; } } return false; } public List getAttachments() { return getAttachments(null); } public List getAttachments(final Predicate filter) { if (mParts.isEmpty()) { return Collections.emptyList(); } final List attachmentParts = new LinkedList<>(); for (final MessagePartData part : mParts) { if (part.isAttachment()) { if (filter == null || filter.apply(part)) { attachmentParts.add(part); } } } return attachmentParts; } public final long getSentTimeStamp() { return mSentTimestamp; } public final long getReceivedTimeStamp() { return mReceivedTimestamp; } public final String getFormattedReceivedTimeStamp() { return Dates.getMessageTimeString(mReceivedTimestamp).toString(); } public final boolean getIsSeen() { return mSeen; } public final boolean getIsRead() { return mRead; } public final boolean getIsMms() { return (mProtocol == MessageData.PROTOCOL_MMS || mProtocol == MessageData.PROTOCOL_MMS_PUSH_NOTIFICATION); } public final boolean getIsMmsNotification() { return (mProtocol == MessageData.PROTOCOL_MMS_PUSH_NOTIFICATION); } public final boolean getIsSms() { return mProtocol == (MessageData.PROTOCOL_SMS); } final int getProtocol() { return mProtocol; } public final int getStatus() { return mStatus; } public final String getSmsMessageUri() { return mSmsMessageUri; } public final int getSmsPriority() { return mSmsPriority; } public final int getSmsMessageSize() { return mSmsMessageSize; } public final String getMmsSubject() { return mMmsSubject; } public final long getMmsExpiry() { return mMmsExpiry; } public final int getRawTelephonyStatus() { return mRawTelephonyStatus; } public final String getSelfParticipantId() { return mSelfParticipantId; } public boolean getIsIncoming() { return (mStatus >= MessageData.BUGLE_STATUS_FIRST_INCOMING); } public boolean hasIncomingErrorStatus() { return (mStatus == MessageData.BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE || mStatus == MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED); } public boolean getIsSendComplete() { return mStatus == MessageData.BUGLE_STATUS_OUTGOING_COMPLETE; } public String getSenderFullName() { return mSenderFullName; } public String getSenderFirstName() { return mSenderFirstName; } public String getSenderDisplayDestination() { return mSenderDisplayDestination; } public String getSenderNormalizedDestination() { return mSenderNormalizedDestination; } public Uri getSenderProfilePhotoUri() { return mSenderProfilePhotoUri == null ? null : Uri.parse(mSenderProfilePhotoUri); } public long getSenderContactId() { return mSenderContactId; } public String getSenderDisplayName() { if (!TextUtils.isEmpty(mSenderFullName)) { return mSenderFullName; } if (!TextUtils.isEmpty(mSenderFirstName)) { return mSenderFirstName; } return mSenderDisplayDestination; } public String getSenderContactLookupKey() { return mSenderContactLookupKey; } public boolean getShowDownloadMessage() { return MessageData.getShowDownloadMessage(mStatus); } public boolean getShowResendMessage() { return MessageData.getShowResendMessage(mStatus); } public boolean getCanForwardMessage() { // Even for outgoing messages, we only allow forwarding if the message has finished sending // as media often has issues when send isn't complete return (mStatus == MessageData.BUGLE_STATUS_OUTGOING_COMPLETE || mStatus == MessageData.BUGLE_STATUS_INCOMING_COMPLETE); } public boolean getCanCopyMessageToClipboard() { return (hasText() && (!getIsIncoming() || mStatus == MessageData.BUGLE_STATUS_INCOMING_COMPLETE)); } public boolean getOneClickResendMessage() { return MessageData.getOneClickResendMessage(mStatus, mRawTelephonyStatus); } /** * Get sender's lookup uri. * This method doesn't support corp contacts. * * @return Lookup uri of sender's contact */ public Uri getSenderContactLookupUri() { if (mSenderContactId > ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED && !TextUtils.isEmpty(mSenderContactLookupKey)) { return ContactsContract.Contacts.getLookupUri(mSenderContactId, mSenderContactLookupKey); } return null; } public boolean getCanClusterWithPreviousMessage() { return mCanClusterWithPreviousMessage; } public boolean getCanClusterWithNextMessage() { return mCanClusterWithNextMessage; } @Override public String toString() { return MessageData.toString(mMessageId, mParts); } // Data definitions public static final String getConversationMessagesQuerySql() { return CONVERSATION_MESSAGES_QUERY_SQL + " AND " // Inject the conversation id + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?)" + CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY; } static final String getConversationMessageIdsQuerySql() { return CONVERSATION_MESSAGES_IDS_QUERY_SQL + " AND " // Inject the conversation id + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?)" + CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY; } public static final String getNotificationQuerySql() { return CONVERSATION_MESSAGES_QUERY_SQL + " AND " + "(" + DatabaseHelper.MessageColumns.STATUS + " in (" + MessageData.BUGLE_STATUS_INCOMING_COMPLETE + ", " + MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD + ")" + " AND " + DatabaseHelper.MessageColumns.SEEN + " = 0)" + ")" + NOTIFICATION_QUERY_SQL_GROUP_BY; } public static final String getWearableQuerySql() { return CONVERSATION_MESSAGES_QUERY_SQL + " AND " + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?" + " AND " + DatabaseHelper.MessageColumns.STATUS + " IN (" + MessageData.BUGLE_STATUS_OUTGOING_DELIVERED + ", " + MessageData.BUGLE_STATUS_OUTGOING_COMPLETE + ", " + MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND + ", " + MessageData.BUGLE_STATUS_OUTGOING_SENDING + ", " + MessageData.BUGLE_STATUS_OUTGOING_RESENDING + ", " + MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY + ", " + MessageData.BUGLE_STATUS_INCOMING_COMPLETE + ", " + MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD + ")" + ")" + NOTIFICATION_QUERY_SQL_GROUP_BY; } /* * Generate a sqlite snippet to call the quote function on the columnName argument. * The columnName doesn't strictly have to be a column name (e.g. it could be an * expression). */ private static String quote(final String columnName) { return "quote(" + columnName + ")"; } private static String makeGroupConcatString(final String column) { return "group_concat(" + column + ", '" + DIVIDER + "')"; } private static String makeIfNullString(final String column) { return "ifnull(" + column + "," + "''" + ")"; } private static String makePartsTableColumnString(final String column) { return DatabaseHelper.PARTS_TABLE + '.' + column; } private static String makeCaseWhenString(final String column, final boolean quote, final String asColumn) { final String fullColumn = makeIfNullString(makePartsTableColumnString(column)); final String groupConcatTerm = quote ? makeGroupConcatString(quote(fullColumn)) : makeGroupConcatString(fullColumn); return "CASE WHEN (" + CONVERSATION_MESSAGE_VIEW_PARTS_COUNT + ">1) THEN " + groupConcatTerm + " ELSE " + makePartsTableColumnString(column) + " END AS " + asColumn; } private static final String CONVERSATION_MESSAGE_VIEW_PARTS_COUNT = "count(" + DatabaseHelper.PARTS_TABLE + '.' + PartColumns._ID + ")"; private static final String EMPTY_STRING = ""; private static final String CONVERSATION_MESSAGES_QUERY_PROJECTION_SQL = DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns._ID + " as " + ConversationMessageViewColumns._ID + ", " + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.CONVERSATION_ID + " as " + ConversationMessageViewColumns.CONVERSATION_ID + ", " + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SENDER_PARTICIPANT_ID + " as " + ConversationMessageViewColumns.PARTICIPANT_ID + ", " + makeCaseWhenString(PartColumns._ID, false, ConversationMessageViewColumns.PARTS_IDS) + ", " + makeCaseWhenString(PartColumns.CONTENT_TYPE, true, ConversationMessageViewColumns.PARTS_CONTENT_TYPES) + ", " + makeCaseWhenString(PartColumns.CONTENT_URI, true, ConversationMessageViewColumns.PARTS_CONTENT_URIS) + ", " + makeCaseWhenString(PartColumns.WIDTH, false, ConversationMessageViewColumns.PARTS_WIDTHS) + ", " + makeCaseWhenString(PartColumns.HEIGHT, false, ConversationMessageViewColumns.PARTS_HEIGHTS) + ", " + makeCaseWhenString(PartColumns.TEXT, true, ConversationMessageViewColumns.PARTS_TEXTS) + ", " + CONVERSATION_MESSAGE_VIEW_PARTS_COUNT + " as " + ConversationMessageViewColumns.PARTS_COUNT + ", " + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SENT_TIMESTAMP + " as " + ConversationMessageViewColumns.SENT_TIMESTAMP + ", " + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP + " as " + ConversationMessageViewColumns.RECEIVED_TIMESTAMP + ", " + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SEEN + " as " + ConversationMessageViewColumns.SEEN + ", " + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.READ + " as " + ConversationMessageViewColumns.READ + ", " + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.PROTOCOL + " as " + ConversationMessageViewColumns.PROTOCOL + ", " + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.STATUS + " as " + ConversationMessageViewColumns.STATUS + ", " + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SMS_MESSAGE_URI + " as " + ConversationMessageViewColumns.SMS_MESSAGE_URI + ", " + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SMS_PRIORITY + " as " + ConversationMessageViewColumns.SMS_PRIORITY + ", " + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SMS_MESSAGE_SIZE + " as " + ConversationMessageViewColumns.SMS_MESSAGE_SIZE + ", " + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.MMS_SUBJECT + " as " + ConversationMessageViewColumns.MMS_SUBJECT + ", " + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.MMS_EXPIRY + " as " + ConversationMessageViewColumns.MMS_EXPIRY + ", " + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RAW_TELEPHONY_STATUS + " as " + ConversationMessageViewColumns.RAW_TELEPHONY_STATUS + ", " + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SELF_PARTICIPANT_ID + " as " + ConversationMessageViewColumns.SELF_PARTICIPANT_ID + ", " + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.FULL_NAME + " as " + ConversationMessageViewColumns.SENDER_FULL_NAME + ", " + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.FIRST_NAME + " as " + ConversationMessageViewColumns.SENDER_FIRST_NAME + ", " + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.DISPLAY_DESTINATION + " as " + ConversationMessageViewColumns.SENDER_DISPLAY_DESTINATION + ", " + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.NORMALIZED_DESTINATION + " as " + ConversationMessageViewColumns.SENDER_NORMALIZED_DESTINATION + ", " + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.PROFILE_PHOTO_URI + " as " + ConversationMessageViewColumns.SENDER_PROFILE_PHOTO_URI + ", " + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.CONTACT_ID + " as " + ConversationMessageViewColumns.SENDER_CONTACT_ID + ", " + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.LOOKUP_KEY + " as " + ConversationMessageViewColumns.SENDER_CONTACT_LOOKUP_KEY + " "; private static final String CONVERSATION_MESSAGES_QUERY_FROM_WHERE_SQL = " FROM " + DatabaseHelper.MESSAGES_TABLE + " LEFT JOIN " + DatabaseHelper.PARTS_TABLE + " ON (" + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns._ID + "=" + DatabaseHelper.PARTS_TABLE + "." + PartColumns.MESSAGE_ID + ") " + " LEFT JOIN " + DatabaseHelper.PARTICIPANTS_TABLE + " ON (" + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SENDER_PARTICIPANT_ID + '=' + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns._ID + ")" // Exclude draft messages from main view + " WHERE (" + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.STATUS + " <> " + MessageData.BUGLE_STATUS_OUTGOING_DRAFT; // This query is mostly static, except for the injection of conversation id. This is for // performance reasons, to ensure that the query uses indices and does not trigger full scans // of the messages table. See b/17160946 for more details. private static final String CONVERSATION_MESSAGES_QUERY_SQL = "SELECT " + CONVERSATION_MESSAGES_QUERY_PROJECTION_SQL + CONVERSATION_MESSAGES_QUERY_FROM_WHERE_SQL; private static final String CONVERSATION_MESSAGE_IDS_PROJECTION_SQL = DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns._ID + " as " + ConversationMessageViewColumns._ID + " "; private static final String CONVERSATION_MESSAGES_IDS_QUERY_SQL = "SELECT " + CONVERSATION_MESSAGE_IDS_PROJECTION_SQL + CONVERSATION_MESSAGES_QUERY_FROM_WHERE_SQL; // Note that we sort DESC and ConversationData reverses the cursor. This is a performance // issue (improvement) for large cursors. private static final String CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY = " GROUP BY " + DatabaseHelper.PARTS_TABLE + '.' + PartColumns.MESSAGE_ID + " ORDER BY " + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP + " DESC"; private static final String NOTIFICATION_QUERY_SQL_GROUP_BY = " GROUP BY " + DatabaseHelper.PARTS_TABLE + '.' + PartColumns.MESSAGE_ID + " ORDER BY " + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP + " DESC"; interface ConversationMessageViewColumns extends BaseColumns { static final String _ID = MessageColumns._ID; static final String CONVERSATION_ID = MessageColumns.CONVERSATION_ID; static final String PARTICIPANT_ID = MessageColumns.SENDER_PARTICIPANT_ID; static final String PARTS_COUNT = "parts_count"; static final String SENT_TIMESTAMP = MessageColumns.SENT_TIMESTAMP; static final String RECEIVED_TIMESTAMP = MessageColumns.RECEIVED_TIMESTAMP; static final String SEEN = MessageColumns.SEEN; static final String READ = MessageColumns.READ; static final String PROTOCOL = MessageColumns.PROTOCOL; static final String STATUS = MessageColumns.STATUS; static final String SMS_MESSAGE_URI = MessageColumns.SMS_MESSAGE_URI; static final String SMS_PRIORITY = MessageColumns.SMS_PRIORITY; static final String SMS_MESSAGE_SIZE = MessageColumns.SMS_MESSAGE_SIZE; static final String MMS_SUBJECT = MessageColumns.MMS_SUBJECT; static final String MMS_EXPIRY = MessageColumns.MMS_EXPIRY; static final String RAW_TELEPHONY_STATUS = MessageColumns.RAW_TELEPHONY_STATUS; static final String SELF_PARTICIPANT_ID = MessageColumns.SELF_PARTICIPANT_ID; static final String SENDER_FULL_NAME = ParticipantColumns.FULL_NAME; static final String SENDER_FIRST_NAME = ParticipantColumns.FIRST_NAME; static final String SENDER_DISPLAY_DESTINATION = ParticipantColumns.DISPLAY_DESTINATION; static final String SENDER_NORMALIZED_DESTINATION = ParticipantColumns.NORMALIZED_DESTINATION; static final String SENDER_PROFILE_PHOTO_URI = ParticipantColumns.PROFILE_PHOTO_URI; static final String SENDER_CONTACT_ID = ParticipantColumns.CONTACT_ID; static final String SENDER_CONTACT_LOOKUP_KEY = ParticipantColumns.LOOKUP_KEY; static final String PARTS_IDS = "parts_ids"; static final String PARTS_CONTENT_TYPES = "parts_content_types"; static final String PARTS_CONTENT_URIS = "parts_content_uris"; static final String PARTS_WIDTHS = "parts_widths"; static final String PARTS_HEIGHTS = "parts_heights"; static final String PARTS_TEXTS = "parts_texts"; } private static int sIndexIncrementer = 0; private static final int INDEX_MESSAGE_ID = sIndexIncrementer++; private static final int INDEX_CONVERSATION_ID = sIndexIncrementer++; private static final int INDEX_PARTICIPANT_ID = sIndexIncrementer++; private static final int INDEX_PARTS_IDS = sIndexIncrementer++; private static final int INDEX_PARTS_CONTENT_TYPES = sIndexIncrementer++; private static final int INDEX_PARTS_CONTENT_URIS = sIndexIncrementer++; private static final int INDEX_PARTS_WIDTHS = sIndexIncrementer++; private static final int INDEX_PARTS_HEIGHTS = sIndexIncrementer++; private static final int INDEX_PARTS_TEXTS = sIndexIncrementer++; private static final int INDEX_PARTS_COUNT = sIndexIncrementer++; private static final int INDEX_SENT_TIMESTAMP = sIndexIncrementer++; private static final int INDEX_RECEIVED_TIMESTAMP = sIndexIncrementer++; private static final int INDEX_SEEN = sIndexIncrementer++; private static final int INDEX_READ = sIndexIncrementer++; private static final int INDEX_PROTOCOL = sIndexIncrementer++; private static final int INDEX_STATUS = sIndexIncrementer++; private static final int INDEX_SMS_MESSAGE_URI = sIndexIncrementer++; private static final int INDEX_SMS_PRIORITY = sIndexIncrementer++; private static final int INDEX_SMS_MESSAGE_SIZE = sIndexIncrementer++; private static final int INDEX_MMS_SUBJECT = sIndexIncrementer++; private static final int INDEX_MMS_EXPIRY = sIndexIncrementer++; private static final int INDEX_RAW_TELEPHONY_STATUS = sIndexIncrementer++; private static final int INDEX_SELF_PARTICIPIANT_ID = sIndexIncrementer++; private static final int INDEX_SENDER_FULL_NAME = sIndexIncrementer++; private static final int INDEX_SENDER_FIRST_NAME = sIndexIncrementer++; private static final int INDEX_SENDER_DISPLAY_DESTINATION = sIndexIncrementer++; private static final int INDEX_SENDER_NORMALIZED_DESTINATION = sIndexIncrementer++; private static final int INDEX_SENDER_PROFILE_PHOTO_URI = sIndexIncrementer++; private static final int INDEX_SENDER_CONTACT_ID = sIndexIncrementer++; private static final int INDEX_SENDER_CONTACT_LOOKUP_KEY = sIndexIncrementer++; private static String[] sProjection = { ConversationMessageViewColumns._ID, ConversationMessageViewColumns.CONVERSATION_ID, ConversationMessageViewColumns.PARTICIPANT_ID, ConversationMessageViewColumns.PARTS_IDS, ConversationMessageViewColumns.PARTS_CONTENT_TYPES, ConversationMessageViewColumns.PARTS_CONTENT_URIS, ConversationMessageViewColumns.PARTS_WIDTHS, ConversationMessageViewColumns.PARTS_HEIGHTS, ConversationMessageViewColumns.PARTS_TEXTS, ConversationMessageViewColumns.PARTS_COUNT, ConversationMessageViewColumns.SENT_TIMESTAMP, ConversationMessageViewColumns.RECEIVED_TIMESTAMP, ConversationMessageViewColumns.SEEN, ConversationMessageViewColumns.READ, ConversationMessageViewColumns.PROTOCOL, ConversationMessageViewColumns.STATUS, ConversationMessageViewColumns.SMS_MESSAGE_URI, ConversationMessageViewColumns.SMS_PRIORITY, ConversationMessageViewColumns.SMS_MESSAGE_SIZE, ConversationMessageViewColumns.MMS_SUBJECT, ConversationMessageViewColumns.MMS_EXPIRY, ConversationMessageViewColumns.RAW_TELEPHONY_STATUS, ConversationMessageViewColumns.SELF_PARTICIPANT_ID, ConversationMessageViewColumns.SENDER_FULL_NAME, ConversationMessageViewColumns.SENDER_FIRST_NAME, ConversationMessageViewColumns.SENDER_DISPLAY_DESTINATION, ConversationMessageViewColumns.SENDER_NORMALIZED_DESTINATION, ConversationMessageViewColumns.SENDER_PROFILE_PHOTO_URI, ConversationMessageViewColumns.SENDER_CONTACT_ID, ConversationMessageViewColumns.SENDER_CONTACT_LOOKUP_KEY, }; public static String[] getProjection() { return sProjection; } }