1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.messaging.datamodel.data; 17 18 import android.database.Cursor; 19 import android.net.Uri; 20 import android.provider.BaseColumns; 21 import android.provider.ContactsContract; 22 import android.text.TextUtils; 23 import android.text.format.DateUtils; 24 25 import com.android.messaging.datamodel.DatabaseHelper; 26 import com.android.messaging.datamodel.DatabaseHelper.MessageColumns; 27 import com.android.messaging.datamodel.DatabaseHelper.PartColumns; 28 import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns; 29 import com.android.messaging.util.Assert; 30 import com.android.messaging.util.BugleGservices; 31 import com.android.messaging.util.BugleGservicesKeys; 32 import com.android.messaging.util.ContentType; 33 import com.android.messaging.util.Dates; 34 import com.android.messaging.util.LogUtil; 35 import com.google.common.annotations.VisibleForTesting; 36 import com.google.common.base.Predicate; 37 38 import java.util.ArrayList; 39 import java.util.Collections; 40 import java.util.LinkedList; 41 import java.util.List; 42 43 /** 44 * Class representing a message within a conversation sequence. The message parts 45 * are available via the getParts() method. 46 * 47 * TODO: See if we can delegate to MessageData for the logic that this class duplicates 48 * (e.g. getIsMms). 49 */ 50 public class ConversationMessageData { 51 private static final String TAG = LogUtil.BUGLE_TAG; 52 53 private String mMessageId; 54 private String mConversationId; 55 private String mParticipantId; 56 private int mPartsCount; 57 private List<MessagePartData> mParts; 58 private long mSentTimestamp; 59 private long mReceivedTimestamp; 60 private boolean mSeen; 61 private boolean mRead; 62 private int mProtocol; 63 private int mStatus; 64 private String mSmsMessageUri; 65 private int mSmsPriority; 66 private int mSmsMessageSize; 67 private String mMmsSubject; 68 private long mMmsExpiry; 69 private int mRawTelephonyStatus; 70 private String mSenderFullName; 71 private String mSenderFirstName; 72 private String mSenderDisplayDestination; 73 private String mSenderNormalizedDestination; 74 private String mSenderProfilePhotoUri; 75 private long mSenderContactId; 76 private String mSenderContactLookupKey; 77 private String mSelfParticipantId; 78 79 /** Are we similar enough to the previous/next messages that we can cluster them? */ 80 private boolean mCanClusterWithPreviousMessage; 81 private boolean mCanClusterWithNextMessage; 82 ConversationMessageData()83 public ConversationMessageData() { 84 } 85 bind(final Cursor cursor)86 public void bind(final Cursor cursor) { 87 mMessageId = cursor.getString(INDEX_MESSAGE_ID); 88 mConversationId = cursor.getString(INDEX_CONVERSATION_ID); 89 mParticipantId = cursor.getString(INDEX_PARTICIPANT_ID); 90 mPartsCount = cursor.getInt(INDEX_PARTS_COUNT); 91 92 mParts = makeParts( 93 cursor.getString(INDEX_PARTS_IDS), 94 cursor.getString(INDEX_PARTS_CONTENT_TYPES), 95 cursor.getString(INDEX_PARTS_CONTENT_URIS), 96 cursor.getString(INDEX_PARTS_WIDTHS), 97 cursor.getString(INDEX_PARTS_HEIGHTS), 98 cursor.getString(INDEX_PARTS_TEXTS), 99 mPartsCount, 100 mMessageId); 101 102 mSentTimestamp = cursor.getLong(INDEX_SENT_TIMESTAMP); 103 mReceivedTimestamp = cursor.getLong(INDEX_RECEIVED_TIMESTAMP); 104 mSeen = (cursor.getInt(INDEX_SEEN) != 0); 105 mRead = (cursor.getInt(INDEX_READ) != 0); 106 mProtocol = cursor.getInt(INDEX_PROTOCOL); 107 mStatus = cursor.getInt(INDEX_STATUS); 108 mSmsMessageUri = cursor.getString(INDEX_SMS_MESSAGE_URI); 109 mSmsPriority = cursor.getInt(INDEX_SMS_PRIORITY); 110 mSmsMessageSize = cursor.getInt(INDEX_SMS_MESSAGE_SIZE); 111 mMmsSubject = cursor.getString(INDEX_MMS_SUBJECT); 112 mMmsExpiry = cursor.getLong(INDEX_MMS_EXPIRY); 113 mRawTelephonyStatus = cursor.getInt(INDEX_RAW_TELEPHONY_STATUS); 114 mSenderFullName = cursor.getString(INDEX_SENDER_FULL_NAME); 115 mSenderFirstName = cursor.getString(INDEX_SENDER_FIRST_NAME); 116 mSenderDisplayDestination = cursor.getString(INDEX_SENDER_DISPLAY_DESTINATION); 117 mSenderNormalizedDestination = cursor.getString(INDEX_SENDER_NORMALIZED_DESTINATION); 118 mSenderProfilePhotoUri = cursor.getString(INDEX_SENDER_PROFILE_PHOTO_URI); 119 mSenderContactId = cursor.getLong(INDEX_SENDER_CONTACT_ID); 120 mSenderContactLookupKey = cursor.getString(INDEX_SENDER_CONTACT_LOOKUP_KEY); 121 mSelfParticipantId = cursor.getString(INDEX_SELF_PARTICIPIANT_ID); 122 123 if (!cursor.isFirst() && cursor.moveToPrevious()) { 124 mCanClusterWithPreviousMessage = canClusterWithMessage(cursor); 125 cursor.moveToNext(); 126 } else { 127 mCanClusterWithPreviousMessage = false; 128 } 129 if (!cursor.isLast() && cursor.moveToNext()) { 130 mCanClusterWithNextMessage = canClusterWithMessage(cursor); 131 cursor.moveToPrevious(); 132 } else { 133 mCanClusterWithNextMessage = false; 134 } 135 } 136 canClusterWithMessage(final Cursor cursor)137 private boolean canClusterWithMessage(final Cursor cursor) { 138 final String otherParticipantId = cursor.getString(INDEX_PARTICIPANT_ID); 139 if (!TextUtils.equals(getParticipantId(), otherParticipantId)) { 140 return false; 141 } 142 final int otherStatus = cursor.getInt(INDEX_STATUS); 143 final boolean otherIsIncoming = (otherStatus >= MessageData.BUGLE_STATUS_FIRST_INCOMING); 144 if (getIsIncoming() != otherIsIncoming) { 145 return false; 146 } 147 final long otherReceivedTimestamp = cursor.getLong(INDEX_RECEIVED_TIMESTAMP); 148 final long timestampDeltaMillis = Math.abs(mReceivedTimestamp - otherReceivedTimestamp); 149 if (timestampDeltaMillis > DateUtils.MINUTE_IN_MILLIS) { 150 return false; 151 } 152 final String otherSelfId = cursor.getString(INDEX_SELF_PARTICIPIANT_ID); 153 if (!TextUtils.equals(getSelfParticipantId(), otherSelfId)) { 154 return false; 155 } 156 return true; 157 } 158 159 private static final Character QUOTE_CHAR = '\''; 160 private static final char DIVIDER = '|'; 161 162 // statics to avoid unnecessary object allocation 163 private static final StringBuilder sUnquoteStringBuilder = new StringBuilder(); 164 private static final ArrayList<String> sUnquoteResults = new ArrayList<String>(); 165 166 // this lock is used to guard access to the above statics 167 private static final Object sUnquoteLock = new Object(); 168 addResult(final ArrayList<String> results, final StringBuilder value)169 private static void addResult(final ArrayList<String> results, final StringBuilder value) { 170 if (value.length() > 0) { 171 results.add(value.toString()); 172 } else { 173 results.add(EMPTY_STRING); 174 } 175 } 176 177 @VisibleForTesting splitUnquotedString(final String inputString)178 static String[] splitUnquotedString(final String inputString) { 179 if (TextUtils.isEmpty(inputString)) { 180 return new String[0]; 181 } 182 183 return inputString.split("\\" + DIVIDER); 184 } 185 186 /** 187 * Takes a group-concated and quoted string and decomposes it into its constituent 188 * parts. A quoted string starts and ends with a single quote. Actual single quotes 189 * within the string are escaped using a second single quote. So, for example, an 190 * input string with 3 constituent parts might look like this: 191 * 192 * 'now is the time'|'I can''t do it'|'foo' 193 * 194 * This would be returned as an array of 3 strings as follows: 195 * now is the time 196 * I can't do it 197 * foo 198 * 199 * This is achieved by walking through the inputString, character by character, 200 * ignoring the outer quotes and the divider and replacing any pair of consecutive 201 * single quotes with a single single quote. 202 * 203 * @param inputString 204 * @return array of constituent strings 205 */ 206 @VisibleForTesting splitQuotedString(final String inputString)207 static String[] splitQuotedString(final String inputString) { 208 if (TextUtils.isEmpty(inputString)) { 209 return new String[0]; 210 } 211 212 // this method can be called from multiple threads but it uses a static 213 // string builder 214 synchronized (sUnquoteLock) { 215 final int length = inputString.length(); 216 final ArrayList<String> results = sUnquoteResults; 217 results.clear(); 218 219 int characterPos = -1; 220 while (++characterPos < length) { 221 final char mustBeQuote = inputString.charAt(characterPos); 222 Assert.isTrue(QUOTE_CHAR == mustBeQuote); 223 while (++characterPos < length) { 224 final char currentChar = inputString.charAt(characterPos); 225 if (currentChar == QUOTE_CHAR) { 226 final char peekAhead = characterPos < length - 1 227 ? inputString.charAt(characterPos + 1) : 0; 228 229 if (peekAhead == QUOTE_CHAR) { 230 characterPos += 1; // skip the second quote 231 } else { 232 addResult(results, sUnquoteStringBuilder); 233 sUnquoteStringBuilder.setLength(0); 234 235 Assert.isTrue((peekAhead == DIVIDER) || (peekAhead == (char) 0)); 236 characterPos += 1; // skip the divider 237 break; 238 } 239 } 240 sUnquoteStringBuilder.append(currentChar); 241 } 242 } 243 return results.toArray(new String[results.size()]); 244 } 245 } 246 247 static MessagePartData makePartData( 248 final String partId, 249 final String contentType, 250 final String contentUriString, 251 final String contentWidth, 252 final String contentHeight, 253 final String text, 254 final String messageId) { 255 if (ContentType.isTextType(contentType)) { 256 final MessagePartData textPart = MessagePartData.createTextMessagePart(text); 257 textPart.updatePartId(partId); 258 textPart.updateMessageId(messageId); 259 return textPart; 260 } else { 261 final Uri contentUri = Uri.parse(contentUriString); 262 final int width = Integer.parseInt(contentWidth); 263 final int height = Integer.parseInt(contentHeight); 264 final MessagePartData attachmentPart = MessagePartData.createMediaMessagePart( 265 contentType, contentUri, width, height); 266 attachmentPart.updatePartId(partId); 267 attachmentPart.updateMessageId(messageId); 268 return attachmentPart; 269 } 270 } 271 272 @VisibleForTesting 273 static List<MessagePartData> makeParts( 274 final String rawIds, 275 final String rawContentTypes, 276 final String rawContentUris, 277 final String rawWidths, 278 final String rawHeights, 279 final String rawTexts, 280 final int partsCount, 281 final String messageId) { 282 final List<MessagePartData> parts = new LinkedList<MessagePartData>(); 283 if (partsCount == 1) { 284 parts.add(makePartData( 285 rawIds, 286 rawContentTypes, 287 rawContentUris, 288 rawWidths, 289 rawHeights, 290 rawTexts, 291 messageId)); 292 } else { 293 unpackMessageParts( 294 parts, 295 splitUnquotedString(rawIds), 296 splitQuotedString(rawContentTypes), 297 splitQuotedString(rawContentUris), 298 splitUnquotedString(rawWidths), 299 splitUnquotedString(rawHeights), 300 splitQuotedString(rawTexts), 301 partsCount, 302 messageId); 303 } 304 return parts; 305 } 306 307 @VisibleForTesting 308 static void unpackMessageParts( 309 final List<MessagePartData> parts, 310 final String[] ids, 311 final String[] contentTypes, 312 final String[] contentUris, 313 final String[] contentWidths, 314 final String[] contentHeights, 315 final String[] texts, 316 final int partsCount, 317 final String messageId) { 318 319 Assert.equals(partsCount, ids.length); 320 Assert.equals(partsCount, contentTypes.length); 321 Assert.equals(partsCount, contentUris.length); 322 Assert.equals(partsCount, contentWidths.length); 323 Assert.equals(partsCount, contentHeights.length); 324 Assert.equals(partsCount, texts.length); 325 326 for (int i = 0; i < partsCount; i++) { 327 parts.add(makePartData( 328 ids[i], 329 contentTypes[i], 330 contentUris[i], 331 contentWidths[i], 332 contentHeights[i], 333 texts[i], 334 messageId)); 335 } 336 337 if (parts.size() != partsCount) { 338 LogUtil.wtf(TAG, "Only unpacked " + parts.size() + " parts from message (id=" 339 + messageId + "), expected " + partsCount + " parts"); 340 } 341 } 342 343 public final String getMessageId() { 344 return mMessageId; 345 } 346 347 public final String getConversationId() { 348 return mConversationId; 349 } 350 351 public final String getParticipantId() { 352 return mParticipantId; 353 } 354 355 public List<MessagePartData> getParts() { 356 return mParts; 357 } 358 359 public boolean hasText() { 360 for (final MessagePartData part : mParts) { 361 if (part.isText()) { 362 return true; 363 } 364 } 365 return false; 366 } 367 368 /** 369 * Get a concatenation of all text parts 370 * 371 * @return the text that is a concatenation of all text parts 372 */ 373 public String getText() { 374 // This is optimized for single text part case, which is the majority 375 376 // For single text part, we just return the part without creating the StringBuilder 377 String firstTextPart = null; 378 boolean foundText = false; 379 // For multiple text parts, we need the StringBuilder and the separator for concatenation 380 StringBuilder sb = null; 381 String separator = null; 382 for (final MessagePartData part : mParts) { 383 if (part.isText()) { 384 if (!foundText) { 385 // First text part 386 firstTextPart = part.getText(); 387 foundText = true; 388 } else { 389 // Second and beyond 390 if (sb == null) { 391 // Need the StringBuilder and the separator starting from 2nd text part 392 sb = new StringBuilder(); 393 if (!TextUtils.isEmpty(firstTextPart)) { 394 sb.append(firstTextPart); 395 } 396 separator = BugleGservices.get().getString( 397 BugleGservicesKeys.MMS_TEXT_CONCAT_SEPARATOR, 398 BugleGservicesKeys.MMS_TEXT_CONCAT_SEPARATOR_DEFAULT); 399 } 400 final String partText = part.getText(); 401 if (!TextUtils.isEmpty(partText)) { 402 if (!TextUtils.isEmpty(separator) && sb.length() > 0) { 403 sb.append(separator); 404 } 405 sb.append(partText); 406 } 407 } 408 } 409 } 410 if (sb == null) { 411 // Only one text part 412 return firstTextPart; 413 } else { 414 // More than one 415 return sb.toString(); 416 } 417 } 418 419 public boolean hasAttachments() { 420 for (final MessagePartData part : mParts) { 421 if (part.isAttachment()) { 422 return true; 423 } 424 } 425 return false; 426 } 427 428 public List<MessagePartData> getAttachments() { 429 return getAttachments(null); 430 } 431 432 public List<MessagePartData> getAttachments(final Predicate<MessagePartData> filter) { 433 if (mParts.isEmpty()) { 434 return Collections.emptyList(); 435 } 436 final List<MessagePartData> attachmentParts = new LinkedList<>(); 437 for (final MessagePartData part : mParts) { 438 if (part.isAttachment()) { 439 if (filter == null || filter.apply(part)) { 440 attachmentParts.add(part); 441 } 442 } 443 } 444 return attachmentParts; 445 } 446 447 public final long getSentTimeStamp() { 448 return mSentTimestamp; 449 } 450 451 public final long getReceivedTimeStamp() { 452 return mReceivedTimestamp; 453 } 454 455 public final String getFormattedReceivedTimeStamp() { 456 return Dates.getMessageTimeString(mReceivedTimestamp).toString(); 457 } 458 459 public final boolean getIsSeen() { 460 return mSeen; 461 } 462 463 public final boolean getIsRead() { 464 return mRead; 465 } 466 467 public final boolean getIsMms() { 468 return (mProtocol == MessageData.PROTOCOL_MMS || 469 mProtocol == MessageData.PROTOCOL_MMS_PUSH_NOTIFICATION); 470 } 471 472 public final boolean getIsMmsNotification() { 473 return (mProtocol == MessageData.PROTOCOL_MMS_PUSH_NOTIFICATION); 474 } 475 476 public final boolean getIsSms() { 477 return mProtocol == (MessageData.PROTOCOL_SMS); 478 } 479 480 final int getProtocol() { 481 return mProtocol; 482 } 483 484 public final int getStatus() { 485 return mStatus; 486 } 487 488 public final String getSmsMessageUri() { 489 return mSmsMessageUri; 490 } 491 492 public final int getSmsPriority() { 493 return mSmsPriority; 494 } 495 496 public final int getSmsMessageSize() { 497 return mSmsMessageSize; 498 } 499 500 public final String getMmsSubject() { 501 return mMmsSubject; 502 } 503 504 public final long getMmsExpiry() { 505 return mMmsExpiry; 506 } 507 508 public final int getRawTelephonyStatus() { 509 return mRawTelephonyStatus; 510 } 511 512 public final String getSelfParticipantId() { 513 return mSelfParticipantId; 514 } 515 516 public boolean getIsIncoming() { 517 return (mStatus >= MessageData.BUGLE_STATUS_FIRST_INCOMING); 518 } 519 hasIncomingErrorStatus()520 public boolean hasIncomingErrorStatus() { 521 return (mStatus == MessageData.BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE || 522 mStatus == MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED); 523 } 524 getIsSendComplete()525 public boolean getIsSendComplete() { 526 return mStatus == MessageData.BUGLE_STATUS_OUTGOING_COMPLETE; 527 } 528 getSenderFullName()529 public String getSenderFullName() { 530 return mSenderFullName; 531 } 532 getSenderFirstName()533 public String getSenderFirstName() { 534 return mSenderFirstName; 535 } 536 getSenderDisplayDestination()537 public String getSenderDisplayDestination() { 538 return mSenderDisplayDestination; 539 } 540 getSenderNormalizedDestination()541 public String getSenderNormalizedDestination() { 542 return mSenderNormalizedDestination; 543 } 544 getSenderProfilePhotoUri()545 public Uri getSenderProfilePhotoUri() { 546 return mSenderProfilePhotoUri == null ? null : Uri.parse(mSenderProfilePhotoUri); 547 } 548 getSenderContactId()549 public long getSenderContactId() { 550 return mSenderContactId; 551 } 552 getSenderDisplayName()553 public String getSenderDisplayName() { 554 if (!TextUtils.isEmpty(mSenderFullName)) { 555 return mSenderFullName; 556 } 557 if (!TextUtils.isEmpty(mSenderFirstName)) { 558 return mSenderFirstName; 559 } 560 return mSenderDisplayDestination; 561 } 562 getSenderContactLookupKey()563 public String getSenderContactLookupKey() { 564 return mSenderContactLookupKey; 565 } 566 getShowDownloadMessage()567 public boolean getShowDownloadMessage() { 568 return MessageData.getShowDownloadMessage(mStatus); 569 } 570 getShowResendMessage()571 public boolean getShowResendMessage() { 572 return MessageData.getShowResendMessage(mStatus); 573 } 574 getCanForwardMessage()575 public boolean getCanForwardMessage() { 576 // Even for outgoing messages, we only allow forwarding if the message has finished sending 577 // as media often has issues when send isn't complete 578 return (mStatus == MessageData.BUGLE_STATUS_OUTGOING_COMPLETE || 579 mStatus == MessageData.BUGLE_STATUS_INCOMING_COMPLETE); 580 } 581 getCanCopyMessageToClipboard()582 public boolean getCanCopyMessageToClipboard() { 583 return (hasText() && 584 (!getIsIncoming() || mStatus == MessageData.BUGLE_STATUS_INCOMING_COMPLETE)); 585 } 586 getOneClickResendMessage()587 public boolean getOneClickResendMessage() { 588 return MessageData.getOneClickResendMessage(mStatus, mRawTelephonyStatus); 589 } 590 591 /** 592 * Get sender's lookup uri. 593 * This method doesn't support corp contacts. 594 * 595 * @return Lookup uri of sender's contact 596 */ getSenderContactLookupUri()597 public Uri getSenderContactLookupUri() { 598 if (mSenderContactId > ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED 599 && !TextUtils.isEmpty(mSenderContactLookupKey)) { 600 return ContactsContract.Contacts.getLookupUri(mSenderContactId, 601 mSenderContactLookupKey); 602 } 603 return null; 604 } 605 getCanClusterWithPreviousMessage()606 public boolean getCanClusterWithPreviousMessage() { 607 return mCanClusterWithPreviousMessage; 608 } 609 getCanClusterWithNextMessage()610 public boolean getCanClusterWithNextMessage() { 611 return mCanClusterWithNextMessage; 612 } 613 614 @Override toString()615 public String toString() { 616 return MessageData.toString(mMessageId, mParts); 617 } 618 619 // Data definitions 620 getConversationMessagesQuerySql()621 public static final String getConversationMessagesQuerySql() { 622 return CONVERSATION_MESSAGES_QUERY_SQL 623 + " AND " 624 // Inject the conversation id 625 + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?)" 626 + CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY; 627 } 628 getConversationMessageIdsQuerySql()629 static final String getConversationMessageIdsQuerySql() { 630 return CONVERSATION_MESSAGES_IDS_QUERY_SQL 631 + " AND " 632 // Inject the conversation id 633 + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?)" 634 + CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY; 635 } 636 getNotificationQuerySql()637 public static final String getNotificationQuerySql() { 638 return CONVERSATION_MESSAGES_QUERY_SQL 639 + " AND " 640 + "(" + DatabaseHelper.MessageColumns.STATUS + " in (" 641 + MessageData.BUGLE_STATUS_INCOMING_COMPLETE + ", " 642 + MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD + ")" 643 + " AND " 644 + DatabaseHelper.MessageColumns.SEEN + " = 0)" 645 + ")" 646 + NOTIFICATION_QUERY_SQL_GROUP_BY; 647 } 648 getWearableQuerySql()649 public static final String getWearableQuerySql() { 650 return CONVERSATION_MESSAGES_QUERY_SQL 651 + " AND " 652 + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?" 653 + " AND " 654 + DatabaseHelper.MessageColumns.STATUS + " IN (" 655 + MessageData.BUGLE_STATUS_OUTGOING_DELIVERED + ", " 656 + MessageData.BUGLE_STATUS_OUTGOING_COMPLETE + ", " 657 + MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND + ", " 658 + MessageData.BUGLE_STATUS_OUTGOING_SENDING + ", " 659 + MessageData.BUGLE_STATUS_OUTGOING_RESENDING + ", " 660 + MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY + ", " 661 + MessageData.BUGLE_STATUS_INCOMING_COMPLETE + ", " 662 + MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD + ")" 663 + ")" 664 + NOTIFICATION_QUERY_SQL_GROUP_BY; 665 } 666 667 /* 668 * Generate a sqlite snippet to call the quote function on the columnName argument. 669 * The columnName doesn't strictly have to be a column name (e.g. it could be an 670 * expression). 671 */ quote(final String columnName)672 private static String quote(final String columnName) { 673 return "quote(" + columnName + ")"; 674 } 675 makeGroupConcatString(final String column)676 private static String makeGroupConcatString(final String column) { 677 return "group_concat(" + column + ", '" + DIVIDER + "')"; 678 } 679 makeIfNullString(final String column)680 private static String makeIfNullString(final String column) { 681 return "ifnull(" + column + "," + "''" + ")"; 682 } 683 makePartsTableColumnString(final String column)684 private static String makePartsTableColumnString(final String column) { 685 return DatabaseHelper.PARTS_TABLE + '.' + column; 686 } 687 makeCaseWhenString(final String column, final boolean quote, final String asColumn)688 private static String makeCaseWhenString(final String column, 689 final boolean quote, 690 final String asColumn) { 691 final String fullColumn = makeIfNullString(makePartsTableColumnString(column)); 692 final String groupConcatTerm = quote 693 ? makeGroupConcatString(quote(fullColumn)) 694 : makeGroupConcatString(fullColumn); 695 return "CASE WHEN (" + CONVERSATION_MESSAGE_VIEW_PARTS_COUNT + ">1) THEN " + groupConcatTerm 696 + " ELSE " + makePartsTableColumnString(column) + " END AS " + asColumn; 697 } 698 699 private static final String CONVERSATION_MESSAGE_VIEW_PARTS_COUNT = 700 "count(" + DatabaseHelper.PARTS_TABLE + '.' + PartColumns._ID + ")"; 701 702 private static final String EMPTY_STRING = ""; 703 704 private static final String CONVERSATION_MESSAGES_QUERY_PROJECTION_SQL = 705 DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns._ID 706 + " as " + ConversationMessageViewColumns._ID + ", " 707 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.CONVERSATION_ID 708 + " as " + ConversationMessageViewColumns.CONVERSATION_ID + ", " 709 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SENDER_PARTICIPANT_ID 710 + " as " + ConversationMessageViewColumns.PARTICIPANT_ID + ", " 711 712 + makeCaseWhenString(PartColumns._ID, false, 713 ConversationMessageViewColumns.PARTS_IDS) + ", " 714 + makeCaseWhenString(PartColumns.CONTENT_TYPE, true, 715 ConversationMessageViewColumns.PARTS_CONTENT_TYPES) + ", " 716 + makeCaseWhenString(PartColumns.CONTENT_URI, true, 717 ConversationMessageViewColumns.PARTS_CONTENT_URIS) + ", " 718 + makeCaseWhenString(PartColumns.WIDTH, false, 719 ConversationMessageViewColumns.PARTS_WIDTHS) + ", " 720 + makeCaseWhenString(PartColumns.HEIGHT, false, 721 ConversationMessageViewColumns.PARTS_HEIGHTS) + ", " 722 + makeCaseWhenString(PartColumns.TEXT, true, 723 ConversationMessageViewColumns.PARTS_TEXTS) + ", " 724 725 + CONVERSATION_MESSAGE_VIEW_PARTS_COUNT 726 + " as " + ConversationMessageViewColumns.PARTS_COUNT + ", " 727 728 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SENT_TIMESTAMP 729 + " as " + ConversationMessageViewColumns.SENT_TIMESTAMP + ", " 730 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP 731 + " as " + ConversationMessageViewColumns.RECEIVED_TIMESTAMP + ", " 732 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SEEN 733 + " as " + ConversationMessageViewColumns.SEEN + ", " 734 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.READ 735 + " as " + ConversationMessageViewColumns.READ + ", " 736 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.PROTOCOL 737 + " as " + ConversationMessageViewColumns.PROTOCOL + ", " 738 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.STATUS 739 + " as " + ConversationMessageViewColumns.STATUS + ", " 740 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SMS_MESSAGE_URI 741 + " as " + ConversationMessageViewColumns.SMS_MESSAGE_URI + ", " 742 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SMS_PRIORITY 743 + " as " + ConversationMessageViewColumns.SMS_PRIORITY + ", " 744 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SMS_MESSAGE_SIZE 745 + " as " + ConversationMessageViewColumns.SMS_MESSAGE_SIZE + ", " 746 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.MMS_SUBJECT 747 + " as " + ConversationMessageViewColumns.MMS_SUBJECT + ", " 748 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.MMS_EXPIRY 749 + " as " + ConversationMessageViewColumns.MMS_EXPIRY + ", " 750 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RAW_TELEPHONY_STATUS 751 + " as " + ConversationMessageViewColumns.RAW_TELEPHONY_STATUS + ", " 752 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SELF_PARTICIPANT_ID 753 + " as " + ConversationMessageViewColumns.SELF_PARTICIPANT_ID + ", " 754 + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.FULL_NAME 755 + " as " + ConversationMessageViewColumns.SENDER_FULL_NAME + ", " 756 + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.FIRST_NAME 757 + " as " + ConversationMessageViewColumns.SENDER_FIRST_NAME + ", " 758 + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.DISPLAY_DESTINATION 759 + " as " + ConversationMessageViewColumns.SENDER_DISPLAY_DESTINATION + ", " 760 + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.NORMALIZED_DESTINATION 761 + " as " + ConversationMessageViewColumns.SENDER_NORMALIZED_DESTINATION + ", " 762 + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.PROFILE_PHOTO_URI 763 + " as " + ConversationMessageViewColumns.SENDER_PROFILE_PHOTO_URI + ", " 764 + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.CONTACT_ID 765 + " as " + ConversationMessageViewColumns.SENDER_CONTACT_ID + ", " 766 + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.LOOKUP_KEY 767 + " as " + ConversationMessageViewColumns.SENDER_CONTACT_LOOKUP_KEY + " "; 768 769 private static final String CONVERSATION_MESSAGES_QUERY_FROM_WHERE_SQL = 770 " FROM " + DatabaseHelper.MESSAGES_TABLE 771 + " LEFT JOIN " + DatabaseHelper.PARTS_TABLE 772 + " ON (" + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns._ID 773 + "=" + DatabaseHelper.PARTS_TABLE + "." + PartColumns.MESSAGE_ID + ") " 774 + " LEFT JOIN " + DatabaseHelper.PARTICIPANTS_TABLE 775 + " ON (" + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SENDER_PARTICIPANT_ID 776 + '=' + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns._ID + ")" 777 // Exclude draft messages from main view 778 + " WHERE (" + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.STATUS 779 + " <> " + MessageData.BUGLE_STATUS_OUTGOING_DRAFT; 780 781 // This query is mostly static, except for the injection of conversation id. This is for 782 // performance reasons, to ensure that the query uses indices and does not trigger full scans 783 // of the messages table. See b/17160946 for more details. 784 private static final String CONVERSATION_MESSAGES_QUERY_SQL = "SELECT " 785 + CONVERSATION_MESSAGES_QUERY_PROJECTION_SQL 786 + CONVERSATION_MESSAGES_QUERY_FROM_WHERE_SQL; 787 788 private static final String CONVERSATION_MESSAGE_IDS_PROJECTION_SQL = 789 DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns._ID 790 + " as " + ConversationMessageViewColumns._ID + " "; 791 792 private static final String CONVERSATION_MESSAGES_IDS_QUERY_SQL = "SELECT " 793 + CONVERSATION_MESSAGE_IDS_PROJECTION_SQL 794 + CONVERSATION_MESSAGES_QUERY_FROM_WHERE_SQL; 795 796 // Note that we sort DESC and ConversationData reverses the cursor. This is a performance 797 // issue (improvement) for large cursors. 798 private static final String CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY = 799 " GROUP BY " + DatabaseHelper.PARTS_TABLE + '.' + PartColumns.MESSAGE_ID 800 + " ORDER BY " 801 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP + " DESC"; 802 803 private static final String NOTIFICATION_QUERY_SQL_GROUP_BY = 804 " GROUP BY " + DatabaseHelper.PARTS_TABLE + '.' + PartColumns.MESSAGE_ID 805 + " ORDER BY " 806 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP + " DESC"; 807 808 interface ConversationMessageViewColumns extends BaseColumns { 809 static final String _ID = MessageColumns._ID; 810 static final String CONVERSATION_ID = MessageColumns.CONVERSATION_ID; 811 static final String PARTICIPANT_ID = MessageColumns.SENDER_PARTICIPANT_ID; 812 static final String PARTS_COUNT = "parts_count"; 813 static final String SENT_TIMESTAMP = MessageColumns.SENT_TIMESTAMP; 814 static final String RECEIVED_TIMESTAMP = MessageColumns.RECEIVED_TIMESTAMP; 815 static final String SEEN = MessageColumns.SEEN; 816 static final String READ = MessageColumns.READ; 817 static final String PROTOCOL = MessageColumns.PROTOCOL; 818 static final String STATUS = MessageColumns.STATUS; 819 static final String SMS_MESSAGE_URI = MessageColumns.SMS_MESSAGE_URI; 820 static final String SMS_PRIORITY = MessageColumns.SMS_PRIORITY; 821 static final String SMS_MESSAGE_SIZE = MessageColumns.SMS_MESSAGE_SIZE; 822 static final String MMS_SUBJECT = MessageColumns.MMS_SUBJECT; 823 static final String MMS_EXPIRY = MessageColumns.MMS_EXPIRY; 824 static final String RAW_TELEPHONY_STATUS = MessageColumns.RAW_TELEPHONY_STATUS; 825 static final String SELF_PARTICIPANT_ID = MessageColumns.SELF_PARTICIPANT_ID; 826 static final String SENDER_FULL_NAME = ParticipantColumns.FULL_NAME; 827 static final String SENDER_FIRST_NAME = ParticipantColumns.FIRST_NAME; 828 static final String SENDER_DISPLAY_DESTINATION = ParticipantColumns.DISPLAY_DESTINATION; 829 static final String SENDER_NORMALIZED_DESTINATION = 830 ParticipantColumns.NORMALIZED_DESTINATION; 831 static final String SENDER_PROFILE_PHOTO_URI = ParticipantColumns.PROFILE_PHOTO_URI; 832 static final String SENDER_CONTACT_ID = ParticipantColumns.CONTACT_ID; 833 static final String SENDER_CONTACT_LOOKUP_KEY = ParticipantColumns.LOOKUP_KEY; 834 static final String PARTS_IDS = "parts_ids"; 835 static final String PARTS_CONTENT_TYPES = "parts_content_types"; 836 static final String PARTS_CONTENT_URIS = "parts_content_uris"; 837 static final String PARTS_WIDTHS = "parts_widths"; 838 static final String PARTS_HEIGHTS = "parts_heights"; 839 static final String PARTS_TEXTS = "parts_texts"; 840 } 841 842 private static int sIndexIncrementer = 0; 843 844 private static final int INDEX_MESSAGE_ID = sIndexIncrementer++; 845 private static final int INDEX_CONVERSATION_ID = sIndexIncrementer++; 846 private static final int INDEX_PARTICIPANT_ID = sIndexIncrementer++; 847 848 private static final int INDEX_PARTS_IDS = sIndexIncrementer++; 849 private static final int INDEX_PARTS_CONTENT_TYPES = sIndexIncrementer++; 850 private static final int INDEX_PARTS_CONTENT_URIS = sIndexIncrementer++; 851 private static final int INDEX_PARTS_WIDTHS = sIndexIncrementer++; 852 private static final int INDEX_PARTS_HEIGHTS = sIndexIncrementer++; 853 private static final int INDEX_PARTS_TEXTS = sIndexIncrementer++; 854 855 private static final int INDEX_PARTS_COUNT = sIndexIncrementer++; 856 857 private static final int INDEX_SENT_TIMESTAMP = sIndexIncrementer++; 858 private static final int INDEX_RECEIVED_TIMESTAMP = sIndexIncrementer++; 859 private static final int INDEX_SEEN = sIndexIncrementer++; 860 private static final int INDEX_READ = sIndexIncrementer++; 861 private static final int INDEX_PROTOCOL = sIndexIncrementer++; 862 private static final int INDEX_STATUS = sIndexIncrementer++; 863 private static final int INDEX_SMS_MESSAGE_URI = sIndexIncrementer++; 864 private static final int INDEX_SMS_PRIORITY = sIndexIncrementer++; 865 private static final int INDEX_SMS_MESSAGE_SIZE = sIndexIncrementer++; 866 private static final int INDEX_MMS_SUBJECT = sIndexIncrementer++; 867 private static final int INDEX_MMS_EXPIRY = sIndexIncrementer++; 868 private static final int INDEX_RAW_TELEPHONY_STATUS = sIndexIncrementer++; 869 private static final int INDEX_SELF_PARTICIPIANT_ID = sIndexIncrementer++; 870 private static final int INDEX_SENDER_FULL_NAME = sIndexIncrementer++; 871 private static final int INDEX_SENDER_FIRST_NAME = sIndexIncrementer++; 872 private static final int INDEX_SENDER_DISPLAY_DESTINATION = sIndexIncrementer++; 873 private static final int INDEX_SENDER_NORMALIZED_DESTINATION = sIndexIncrementer++; 874 private static final int INDEX_SENDER_PROFILE_PHOTO_URI = sIndexIncrementer++; 875 private static final int INDEX_SENDER_CONTACT_ID = sIndexIncrementer++; 876 private static final int INDEX_SENDER_CONTACT_LOOKUP_KEY = sIndexIncrementer++; 877 878 879 private static String[] sProjection = { 880 ConversationMessageViewColumns._ID, 881 ConversationMessageViewColumns.CONVERSATION_ID, 882 ConversationMessageViewColumns.PARTICIPANT_ID, 883 884 ConversationMessageViewColumns.PARTS_IDS, 885 ConversationMessageViewColumns.PARTS_CONTENT_TYPES, 886 ConversationMessageViewColumns.PARTS_CONTENT_URIS, 887 ConversationMessageViewColumns.PARTS_WIDTHS, 888 ConversationMessageViewColumns.PARTS_HEIGHTS, 889 ConversationMessageViewColumns.PARTS_TEXTS, 890 891 ConversationMessageViewColumns.PARTS_COUNT, 892 ConversationMessageViewColumns.SENT_TIMESTAMP, 893 ConversationMessageViewColumns.RECEIVED_TIMESTAMP, 894 ConversationMessageViewColumns.SEEN, 895 ConversationMessageViewColumns.READ, 896 ConversationMessageViewColumns.PROTOCOL, 897 ConversationMessageViewColumns.STATUS, 898 ConversationMessageViewColumns.SMS_MESSAGE_URI, 899 ConversationMessageViewColumns.SMS_PRIORITY, 900 ConversationMessageViewColumns.SMS_MESSAGE_SIZE, 901 ConversationMessageViewColumns.MMS_SUBJECT, 902 ConversationMessageViewColumns.MMS_EXPIRY, 903 ConversationMessageViewColumns.RAW_TELEPHONY_STATUS, 904 ConversationMessageViewColumns.SELF_PARTICIPANT_ID, 905 ConversationMessageViewColumns.SENDER_FULL_NAME, 906 ConversationMessageViewColumns.SENDER_FIRST_NAME, 907 ConversationMessageViewColumns.SENDER_DISPLAY_DESTINATION, 908 ConversationMessageViewColumns.SENDER_NORMALIZED_DESTINATION, 909 ConversationMessageViewColumns.SENDER_PROFILE_PHOTO_URI, 910 ConversationMessageViewColumns.SENDER_CONTACT_ID, 911 ConversationMessageViewColumns.SENDER_CONTACT_LOOKUP_KEY, 912 }; 913 getProjection()914 public static String[] getProjection() { 915 return sProjection; 916 } 917 } 918