921 lines
38 KiB
Java
921 lines
38 KiB
Java
/*
|
|
* Copyright (C) 2015 The Android Open Source Project
|
|
* Copyright (C) 2024 The LineageOS 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 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 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 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 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 {
|
|
String _ID = MessageColumns._ID;
|
|
String CONVERSATION_ID = MessageColumns.CONVERSATION_ID;
|
|
String PARTICIPANT_ID = MessageColumns.SENDER_PARTICIPANT_ID;
|
|
String PARTS_COUNT = "parts_count";
|
|
String SENT_TIMESTAMP = MessageColumns.SENT_TIMESTAMP;
|
|
String RECEIVED_TIMESTAMP = MessageColumns.RECEIVED_TIMESTAMP;
|
|
String SEEN = MessageColumns.SEEN;
|
|
String READ = MessageColumns.READ;
|
|
String PROTOCOL = MessageColumns.PROTOCOL;
|
|
String STATUS = MessageColumns.STATUS;
|
|
String SMS_MESSAGE_URI = MessageColumns.SMS_MESSAGE_URI;
|
|
String SMS_PRIORITY = MessageColumns.SMS_PRIORITY;
|
|
String SMS_MESSAGE_SIZE = MessageColumns.SMS_MESSAGE_SIZE;
|
|
String MMS_SUBJECT = MessageColumns.MMS_SUBJECT;
|
|
String MMS_EXPIRY = MessageColumns.MMS_EXPIRY;
|
|
String RAW_TELEPHONY_STATUS = MessageColumns.RAW_TELEPHONY_STATUS;
|
|
String SELF_PARTICIPANT_ID = MessageColumns.SELF_PARTICIPANT_ID;
|
|
String SENDER_FULL_NAME = ParticipantColumns.FULL_NAME;
|
|
String SENDER_FIRST_NAME = ParticipantColumns.FIRST_NAME;
|
|
String SENDER_DISPLAY_DESTINATION = ParticipantColumns.DISPLAY_DESTINATION;
|
|
String SENDER_NORMALIZED_DESTINATION =
|
|
ParticipantColumns.NORMALIZED_DESTINATION;
|
|
String SENDER_PROFILE_PHOTO_URI = ParticipantColumns.PROFILE_PHOTO_URI;
|
|
String SENDER_CONTACT_ID = ParticipantColumns.CONTACT_ID;
|
|
String SENDER_CONTACT_LOOKUP_KEY = ParticipantColumns.LOOKUP_KEY;
|
|
String PARTS_IDS = "parts_ids";
|
|
String PARTS_CONTENT_TYPES = "parts_content_types";
|
|
String PARTS_CONTENT_URIS = "parts_content_uris";
|
|
String PARTS_WIDTHS = "parts_widths";
|
|
String PARTS_HEIGHTS = "parts_heights";
|
|
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 final 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;
|
|
}
|
|
}
|