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 || mStatus == MessageData.BUGLE_STATUS_OUTGOING_DELIVERED); 528 } 529 getSenderFullName()530 public String getSenderFullName() { 531 return mSenderFullName; 532 } 533 getSenderFirstName()534 public String getSenderFirstName() { 535 return mSenderFirstName; 536 } 537 getSenderDisplayDestination()538 public String getSenderDisplayDestination() { 539 return mSenderDisplayDestination; 540 } 541 getSenderNormalizedDestination()542 public String getSenderNormalizedDestination() { 543 return mSenderNormalizedDestination; 544 } 545 getSenderProfilePhotoUri()546 public Uri getSenderProfilePhotoUri() { 547 return mSenderProfilePhotoUri == null ? null : Uri.parse(mSenderProfilePhotoUri); 548 } 549 getSenderContactId()550 public long getSenderContactId() { 551 return mSenderContactId; 552 } 553 getSenderDisplayName()554 public String getSenderDisplayName() { 555 if (!TextUtils.isEmpty(mSenderFullName)) { 556 return mSenderFullName; 557 } 558 if (!TextUtils.isEmpty(mSenderFirstName)) { 559 return mSenderFirstName; 560 } 561 return mSenderDisplayDestination; 562 } 563 getSenderContactLookupKey()564 public String getSenderContactLookupKey() { 565 return mSenderContactLookupKey; 566 } 567 getShowDownloadMessage()568 public boolean getShowDownloadMessage() { 569 return MessageData.getShowDownloadMessage(mStatus); 570 } 571 getShowResendMessage()572 public boolean getShowResendMessage() { 573 return MessageData.getShowResendMessage(mStatus); 574 } 575 getCanForwardMessage()576 public boolean getCanForwardMessage() { 577 // Even for outgoing messages, we only allow forwarding if the message has finished sending 578 // as media often has issues when send isn't complete 579 return (mStatus == MessageData.BUGLE_STATUS_OUTGOING_COMPLETE 580 || mStatus == MessageData.BUGLE_STATUS_OUTGOING_DELIVERED 581 || mStatus == MessageData.BUGLE_STATUS_INCOMING_COMPLETE); 582 } 583 getCanCopyMessageToClipboard()584 public boolean getCanCopyMessageToClipboard() { 585 return (hasText() && 586 (!getIsIncoming() || mStatus == MessageData.BUGLE_STATUS_INCOMING_COMPLETE)); 587 } 588 getOneClickResendMessage()589 public boolean getOneClickResendMessage() { 590 return MessageData.getOneClickResendMessage(mStatus, mRawTelephonyStatus); 591 } 592 593 /** 594 * Get sender's lookup uri. 595 * This method doesn't support corp contacts. 596 * 597 * @return Lookup uri of sender's contact 598 */ getSenderContactLookupUri()599 public Uri getSenderContactLookupUri() { 600 if (mSenderContactId > ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED 601 && !TextUtils.isEmpty(mSenderContactLookupKey)) { 602 return ContactsContract.Contacts.getLookupUri(mSenderContactId, 603 mSenderContactLookupKey); 604 } 605 return null; 606 } 607 getCanClusterWithPreviousMessage()608 public boolean getCanClusterWithPreviousMessage() { 609 return mCanClusterWithPreviousMessage; 610 } 611 getCanClusterWithNextMessage()612 public boolean getCanClusterWithNextMessage() { 613 return mCanClusterWithNextMessage; 614 } 615 616 @Override toString()617 public String toString() { 618 return MessageData.toString(mMessageId, mParts); 619 } 620 621 // Data definitions 622 getConversationMessagesQuerySql()623 public static final String getConversationMessagesQuerySql() { 624 return CONVERSATION_MESSAGES_QUERY_SQL 625 + " AND " 626 // Inject the conversation id 627 + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?)" 628 + CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY; 629 } 630 getConversationMessageIdsQuerySql()631 static final String getConversationMessageIdsQuerySql() { 632 return CONVERSATION_MESSAGES_IDS_QUERY_SQL 633 + " AND " 634 // Inject the conversation id 635 + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?)" 636 + CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY; 637 } 638 getNotificationQuerySql()639 public static final String getNotificationQuerySql() { 640 return CONVERSATION_MESSAGES_QUERY_SQL 641 + " AND " 642 + "(" + DatabaseHelper.MessageColumns.STATUS + " in (" 643 + MessageData.BUGLE_STATUS_INCOMING_COMPLETE + ", " 644 + MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD + ")" 645 + " AND " 646 + DatabaseHelper.MessageColumns.SEEN + " = 0)" 647 + ")" 648 + NOTIFICATION_QUERY_SQL_GROUP_BY; 649 } 650 getWearableQuerySql()651 public static final String getWearableQuerySql() { 652 return CONVERSATION_MESSAGES_QUERY_SQL 653 + " AND " 654 + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?" 655 + " AND " 656 + DatabaseHelper.MessageColumns.STATUS + " IN (" 657 + MessageData.BUGLE_STATUS_OUTGOING_DELIVERED + ", " 658 + MessageData.BUGLE_STATUS_OUTGOING_COMPLETE + ", " 659 + MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND + ", " 660 + MessageData.BUGLE_STATUS_OUTGOING_SENDING + ", " 661 + MessageData.BUGLE_STATUS_OUTGOING_RESENDING + ", " 662 + MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY + ", " 663 + MessageData.BUGLE_STATUS_INCOMING_COMPLETE + ", " 664 + MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD + ")" 665 + ")" 666 + NOTIFICATION_QUERY_SQL_GROUP_BY; 667 } 668 669 /* 670 * Generate a sqlite snippet to call the quote function on the columnName argument. 671 * The columnName doesn't strictly have to be a column name (e.g. it could be an 672 * expression). 673 */ quote(final String columnName)674 private static String quote(final String columnName) { 675 return "quote(" + columnName + ")"; 676 } 677 makeGroupConcatString(final String column)678 private static String makeGroupConcatString(final String column) { 679 return "group_concat(" + column + ", '" + DIVIDER + "')"; 680 } 681 makeIfNullString(final String column)682 private static String makeIfNullString(final String column) { 683 return "ifnull(" + column + "," + "''" + ")"; 684 } 685 makePartsTableColumnString(final String column)686 private static String makePartsTableColumnString(final String column) { 687 return DatabaseHelper.PARTS_TABLE + '.' + column; 688 } 689 makeCaseWhenString(final String column, final boolean quote, final String asColumn)690 private static String makeCaseWhenString(final String column, 691 final boolean quote, 692 final String asColumn) { 693 final String fullColumn = makeIfNullString(makePartsTableColumnString(column)); 694 final String groupConcatTerm = quote 695 ? makeGroupConcatString(quote(fullColumn)) 696 : makeGroupConcatString(fullColumn); 697 return "CASE WHEN (" + CONVERSATION_MESSAGE_VIEW_PARTS_COUNT + ">1) THEN " + groupConcatTerm 698 + " ELSE " + makePartsTableColumnString(column) + " END AS " + asColumn; 699 } 700 701 private static final String CONVERSATION_MESSAGE_VIEW_PARTS_COUNT = 702 "count(" + DatabaseHelper.PARTS_TABLE + '.' + PartColumns._ID + ")"; 703 704 private static final String EMPTY_STRING = ""; 705 706 private static final String CONVERSATION_MESSAGES_QUERY_PROJECTION_SQL = 707 DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns._ID 708 + " as " + ConversationMessageViewColumns._ID + ", " 709 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.CONVERSATION_ID 710 + " as " + ConversationMessageViewColumns.CONVERSATION_ID + ", " 711 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SENDER_PARTICIPANT_ID 712 + " as " + ConversationMessageViewColumns.PARTICIPANT_ID + ", " 713 714 + makeCaseWhenString(PartColumns._ID, false, 715 ConversationMessageViewColumns.PARTS_IDS) + ", " 716 + makeCaseWhenString(PartColumns.CONTENT_TYPE, true, 717 ConversationMessageViewColumns.PARTS_CONTENT_TYPES) + ", " 718 + makeCaseWhenString(PartColumns.CONTENT_URI, true, 719 ConversationMessageViewColumns.PARTS_CONTENT_URIS) + ", " 720 + makeCaseWhenString(PartColumns.WIDTH, false, 721 ConversationMessageViewColumns.PARTS_WIDTHS) + ", " 722 + makeCaseWhenString(PartColumns.HEIGHT, false, 723 ConversationMessageViewColumns.PARTS_HEIGHTS) + ", " 724 + makeCaseWhenString(PartColumns.TEXT, true, 725 ConversationMessageViewColumns.PARTS_TEXTS) + ", " 726 727 + CONVERSATION_MESSAGE_VIEW_PARTS_COUNT 728 + " as " + ConversationMessageViewColumns.PARTS_COUNT + ", " 729 730 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SENT_TIMESTAMP 731 + " as " + ConversationMessageViewColumns.SENT_TIMESTAMP + ", " 732 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP 733 + " as " + ConversationMessageViewColumns.RECEIVED_TIMESTAMP + ", " 734 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SEEN 735 + " as " + ConversationMessageViewColumns.SEEN + ", " 736 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.READ 737 + " as " + ConversationMessageViewColumns.READ + ", " 738 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.PROTOCOL 739 + " as " + ConversationMessageViewColumns.PROTOCOL + ", " 740 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.STATUS 741 + " as " + ConversationMessageViewColumns.STATUS + ", " 742 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SMS_MESSAGE_URI 743 + " as " + ConversationMessageViewColumns.SMS_MESSAGE_URI + ", " 744 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SMS_PRIORITY 745 + " as " + ConversationMessageViewColumns.SMS_PRIORITY + ", " 746 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SMS_MESSAGE_SIZE 747 + " as " + ConversationMessageViewColumns.SMS_MESSAGE_SIZE + ", " 748 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.MMS_SUBJECT 749 + " as " + ConversationMessageViewColumns.MMS_SUBJECT + ", " 750 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.MMS_EXPIRY 751 + " as " + ConversationMessageViewColumns.MMS_EXPIRY + ", " 752 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RAW_TELEPHONY_STATUS 753 + " as " + ConversationMessageViewColumns.RAW_TELEPHONY_STATUS + ", " 754 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SELF_PARTICIPANT_ID 755 + " as " + ConversationMessageViewColumns.SELF_PARTICIPANT_ID + ", " 756 + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.FULL_NAME 757 + " as " + ConversationMessageViewColumns.SENDER_FULL_NAME + ", " 758 + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.FIRST_NAME 759 + " as " + ConversationMessageViewColumns.SENDER_FIRST_NAME + ", " 760 + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.DISPLAY_DESTINATION 761 + " as " + ConversationMessageViewColumns.SENDER_DISPLAY_DESTINATION + ", " 762 + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.NORMALIZED_DESTINATION 763 + " as " + ConversationMessageViewColumns.SENDER_NORMALIZED_DESTINATION + ", " 764 + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.PROFILE_PHOTO_URI 765 + " as " + ConversationMessageViewColumns.SENDER_PROFILE_PHOTO_URI + ", " 766 + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.CONTACT_ID 767 + " as " + ConversationMessageViewColumns.SENDER_CONTACT_ID + ", " 768 + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.LOOKUP_KEY 769 + " as " + ConversationMessageViewColumns.SENDER_CONTACT_LOOKUP_KEY + " "; 770 771 private static final String CONVERSATION_MESSAGES_QUERY_FROM_WHERE_SQL = 772 " FROM " + DatabaseHelper.MESSAGES_TABLE 773 + " LEFT JOIN " + DatabaseHelper.PARTS_TABLE 774 + " ON (" + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns._ID 775 + "=" + DatabaseHelper.PARTS_TABLE + "." + PartColumns.MESSAGE_ID + ") " 776 + " LEFT JOIN " + DatabaseHelper.PARTICIPANTS_TABLE 777 + " ON (" + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SENDER_PARTICIPANT_ID 778 + '=' + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns._ID + ")" 779 // Exclude draft messages from main view 780 + " WHERE (" + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.STATUS 781 + " <> " + MessageData.BUGLE_STATUS_OUTGOING_DRAFT; 782 783 // This query is mostly static, except for the injection of conversation id. This is for 784 // performance reasons, to ensure that the query uses indices and does not trigger full scans 785 // of the messages table. See b/17160946 for more details. 786 private static final String CONVERSATION_MESSAGES_QUERY_SQL = "SELECT " 787 + CONVERSATION_MESSAGES_QUERY_PROJECTION_SQL 788 + CONVERSATION_MESSAGES_QUERY_FROM_WHERE_SQL; 789 790 private static final String CONVERSATION_MESSAGE_IDS_PROJECTION_SQL = 791 DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns._ID 792 + " as " + ConversationMessageViewColumns._ID + " "; 793 794 private static final String CONVERSATION_MESSAGES_IDS_QUERY_SQL = "SELECT " 795 + CONVERSATION_MESSAGE_IDS_PROJECTION_SQL 796 + CONVERSATION_MESSAGES_QUERY_FROM_WHERE_SQL; 797 798 // Note that we sort DESC and ConversationData reverses the cursor. This is a performance 799 // issue (improvement) for large cursors. 800 private static final String CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY = 801 " GROUP BY " + DatabaseHelper.PARTS_TABLE + '.' + PartColumns.MESSAGE_ID 802 + " ORDER BY " 803 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP + " DESC"; 804 805 private static final String NOTIFICATION_QUERY_SQL_GROUP_BY = 806 " GROUP BY " + DatabaseHelper.PARTS_TABLE + '.' + PartColumns.MESSAGE_ID 807 + " ORDER BY " 808 + DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP + " DESC"; 809 810 interface ConversationMessageViewColumns extends BaseColumns { 811 static final String _ID = MessageColumns._ID; 812 static final String CONVERSATION_ID = MessageColumns.CONVERSATION_ID; 813 static final String PARTICIPANT_ID = MessageColumns.SENDER_PARTICIPANT_ID; 814 static final String PARTS_COUNT = "parts_count"; 815 static final String SENT_TIMESTAMP = MessageColumns.SENT_TIMESTAMP; 816 static final String RECEIVED_TIMESTAMP = MessageColumns.RECEIVED_TIMESTAMP; 817 static final String SEEN = MessageColumns.SEEN; 818 static final String READ = MessageColumns.READ; 819 static final String PROTOCOL = MessageColumns.PROTOCOL; 820 static final String STATUS = MessageColumns.STATUS; 821 static final String SMS_MESSAGE_URI = MessageColumns.SMS_MESSAGE_URI; 822 static final String SMS_PRIORITY = MessageColumns.SMS_PRIORITY; 823 static final String SMS_MESSAGE_SIZE = MessageColumns.SMS_MESSAGE_SIZE; 824 static final String MMS_SUBJECT = MessageColumns.MMS_SUBJECT; 825 static final String MMS_EXPIRY = MessageColumns.MMS_EXPIRY; 826 static final String RAW_TELEPHONY_STATUS = MessageColumns.RAW_TELEPHONY_STATUS; 827 static final String SELF_PARTICIPANT_ID = MessageColumns.SELF_PARTICIPANT_ID; 828 static final String SENDER_FULL_NAME = ParticipantColumns.FULL_NAME; 829 static final String SENDER_FIRST_NAME = ParticipantColumns.FIRST_NAME; 830 static final String SENDER_DISPLAY_DESTINATION = ParticipantColumns.DISPLAY_DESTINATION; 831 static final String SENDER_NORMALIZED_DESTINATION = 832 ParticipantColumns.NORMALIZED_DESTINATION; 833 static final String SENDER_PROFILE_PHOTO_URI = ParticipantColumns.PROFILE_PHOTO_URI; 834 static final String SENDER_CONTACT_ID = ParticipantColumns.CONTACT_ID; 835 static final String SENDER_CONTACT_LOOKUP_KEY = ParticipantColumns.LOOKUP_KEY; 836 static final String PARTS_IDS = "parts_ids"; 837 static final String PARTS_CONTENT_TYPES = "parts_content_types"; 838 static final String PARTS_CONTENT_URIS = "parts_content_uris"; 839 static final String PARTS_WIDTHS = "parts_widths"; 840 static final String PARTS_HEIGHTS = "parts_heights"; 841 static final String PARTS_TEXTS = "parts_texts"; 842 } 843 844 private static int sIndexIncrementer = 0; 845 846 private static final int INDEX_MESSAGE_ID = sIndexIncrementer++; 847 private static final int INDEX_CONVERSATION_ID = sIndexIncrementer++; 848 private static final int INDEX_PARTICIPANT_ID = sIndexIncrementer++; 849 850 private static final int INDEX_PARTS_IDS = sIndexIncrementer++; 851 private static final int INDEX_PARTS_CONTENT_TYPES = sIndexIncrementer++; 852 private static final int INDEX_PARTS_CONTENT_URIS = sIndexIncrementer++; 853 private static final int INDEX_PARTS_WIDTHS = sIndexIncrementer++; 854 private static final int INDEX_PARTS_HEIGHTS = sIndexIncrementer++; 855 private static final int INDEX_PARTS_TEXTS = sIndexIncrementer++; 856 857 private static final int INDEX_PARTS_COUNT = sIndexIncrementer++; 858 859 private static final int INDEX_SENT_TIMESTAMP = sIndexIncrementer++; 860 private static final int INDEX_RECEIVED_TIMESTAMP = sIndexIncrementer++; 861 private static final int INDEX_SEEN = sIndexIncrementer++; 862 private static final int INDEX_READ = sIndexIncrementer++; 863 private static final int INDEX_PROTOCOL = sIndexIncrementer++; 864 private static final int INDEX_STATUS = sIndexIncrementer++; 865 private static final int INDEX_SMS_MESSAGE_URI = sIndexIncrementer++; 866 private static final int INDEX_SMS_PRIORITY = sIndexIncrementer++; 867 private static final int INDEX_SMS_MESSAGE_SIZE = sIndexIncrementer++; 868 private static final int INDEX_MMS_SUBJECT = sIndexIncrementer++; 869 private static final int INDEX_MMS_EXPIRY = sIndexIncrementer++; 870 private static final int INDEX_RAW_TELEPHONY_STATUS = sIndexIncrementer++; 871 private static final int INDEX_SELF_PARTICIPIANT_ID = sIndexIncrementer++; 872 private static final int INDEX_SENDER_FULL_NAME = sIndexIncrementer++; 873 private static final int INDEX_SENDER_FIRST_NAME = sIndexIncrementer++; 874 private static final int INDEX_SENDER_DISPLAY_DESTINATION = sIndexIncrementer++; 875 private static final int INDEX_SENDER_NORMALIZED_DESTINATION = sIndexIncrementer++; 876 private static final int INDEX_SENDER_PROFILE_PHOTO_URI = sIndexIncrementer++; 877 private static final int INDEX_SENDER_CONTACT_ID = sIndexIncrementer++; 878 private static final int INDEX_SENDER_CONTACT_LOOKUP_KEY = sIndexIncrementer++; 879 880 881 private static String[] sProjection = { 882 ConversationMessageViewColumns._ID, 883 ConversationMessageViewColumns.CONVERSATION_ID, 884 ConversationMessageViewColumns.PARTICIPANT_ID, 885 886 ConversationMessageViewColumns.PARTS_IDS, 887 ConversationMessageViewColumns.PARTS_CONTENT_TYPES, 888 ConversationMessageViewColumns.PARTS_CONTENT_URIS, 889 ConversationMessageViewColumns.PARTS_WIDTHS, 890 ConversationMessageViewColumns.PARTS_HEIGHTS, 891 ConversationMessageViewColumns.PARTS_TEXTS, 892 893 ConversationMessageViewColumns.PARTS_COUNT, 894 ConversationMessageViewColumns.SENT_TIMESTAMP, 895 ConversationMessageViewColumns.RECEIVED_TIMESTAMP, 896 ConversationMessageViewColumns.SEEN, 897 ConversationMessageViewColumns.READ, 898 ConversationMessageViewColumns.PROTOCOL, 899 ConversationMessageViewColumns.STATUS, 900 ConversationMessageViewColumns.SMS_MESSAGE_URI, 901 ConversationMessageViewColumns.SMS_PRIORITY, 902 ConversationMessageViewColumns.SMS_MESSAGE_SIZE, 903 ConversationMessageViewColumns.MMS_SUBJECT, 904 ConversationMessageViewColumns.MMS_EXPIRY, 905 ConversationMessageViewColumns.RAW_TELEPHONY_STATUS, 906 ConversationMessageViewColumns.SELF_PARTICIPANT_ID, 907 ConversationMessageViewColumns.SENDER_FULL_NAME, 908 ConversationMessageViewColumns.SENDER_FIRST_NAME, 909 ConversationMessageViewColumns.SENDER_DISPLAY_DESTINATION, 910 ConversationMessageViewColumns.SENDER_NORMALIZED_DESTINATION, 911 ConversationMessageViewColumns.SENDER_PROFILE_PHOTO_URI, 912 ConversationMessageViewColumns.SENDER_CONTACT_ID, 913 ConversationMessageViewColumns.SENDER_CONTACT_LOOKUP_KEY, 914 }; 915 getProjection()916 public static String[] getProjection() { 917 return sProjection; 918 } 919 } 920