| /* |
| * 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<MessagePartData> 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<String> sUnquoteResults = new ArrayList<String>(); |
| |
| // this lock is used to guard access to the above statics |
| private static final Object sUnquoteLock = new Object(); |
| |
| private static void addResult(final ArrayList<String> 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<String> 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<MessagePartData> 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<MessagePartData> parts = new LinkedList<MessagePartData>(); |
| 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<MessagePartData> 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<MessagePartData> 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<MessagePartData> getAttachments() { |
| return getAttachments(null); |
| } |
| |
| public List<MessagePartData> getAttachments(final Predicate<MessagePartData> filter) { |
| if (mParts.isEmpty()) { |
| return Collections.emptyList(); |
| } |
| final List<MessagePartData> 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 |
| || mStatus == MessageData.BUGLE_STATUS_OUTGOING_DELIVERED); |
| } |
| |
| 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_OUTGOING_DELIVERED |
| || 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; |
| } |
| } |