1 /** 2 * Copyright (c) 2012, Google Inc. 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 17 package com.android.mail.providers; 18 19 import android.content.ContentValues; 20 import android.content.Context; 21 import android.database.Cursor; 22 import android.net.Uri; 23 import android.os.Bundle; 24 import android.os.Parcel; 25 import android.os.Parcelable; 26 import android.provider.BaseColumns; 27 import android.text.TextUtils; 28 29 import com.android.mail.R; 30 import com.android.mail.browse.ConversationCursor; 31 import com.android.mail.content.CursorCreator; 32 import com.android.mail.providers.UIProvider.ConversationColumns; 33 import com.android.mail.providers.UIProvider.ConversationCursorCommand; 34 import com.android.mail.ui.ConversationCursorLoader; 35 import com.android.mail.utils.LogTag; 36 import com.android.mail.utils.LogUtils; 37 import com.google.common.collect.ImmutableList; 38 39 import java.util.Collection; 40 import java.util.Collections; 41 import java.util.List; 42 43 public class Conversation implements Parcelable { 44 public static final int NO_POSITION = -1; 45 46 private static final String LOG_TAG = LogTag.getLogTag(); 47 48 private static final String EMPTY_STRING = ""; 49 50 /** 51 * @see BaseColumns#_ID 52 */ 53 public final long id; 54 /** 55 * @see UIProvider.ConversationColumns#URI 56 */ 57 public final Uri uri; 58 /** 59 * @see UIProvider.ConversationColumns#SUBJECT 60 */ 61 public final String subject; 62 /** 63 * @see UIProvider.ConversationColumns#DATE_RECEIVED_MS 64 */ 65 public final long dateMs; 66 /** 67 * @see UIProvider.ConversationColumns#HAS_ATTACHMENTS 68 */ 69 public final boolean hasAttachments; 70 /** 71 * @see UIProvider.ConversationColumns#MESSAGE_LIST_URI 72 */ 73 public final Uri messageListUri; 74 /** 75 * @see UIProvider.ConversationColumns#SENDING_STATE 76 */ 77 public final int sendingState; 78 /** 79 * @see UIProvider.ConversationColumns#PRIORITY 80 */ 81 public int priority; 82 /** 83 * @see UIProvider.ConversationColumns#READ 84 */ 85 public boolean read; 86 /** 87 * @see UIProvider.ConversationColumns#SEEN 88 */ 89 public boolean seen; 90 /** 91 * @see UIProvider.ConversationColumns#STARRED 92 */ 93 public boolean starred; 94 /** 95 * @see UIProvider.ConversationColumns#RAW_FOLDERS 96 */ 97 private FolderList rawFolders; 98 /** 99 * @see UIProvider.ConversationColumns#FLAGS 100 */ 101 public int convFlags; 102 /** 103 * @see UIProvider.ConversationColumns#PERSONAL_LEVEL 104 */ 105 public final int personalLevel; 106 /** 107 * @see UIProvider.ConversationColumns#SPAM 108 */ 109 public final boolean spam; 110 /** 111 * @see UIProvider.ConversationColumns#MUTED 112 */ 113 public final boolean muted; 114 /** 115 * @see UIProvider.ConversationColumns#PHISHING 116 */ 117 public final boolean phishing; 118 /** 119 * @see UIProvider.ConversationColumns#COLOR 120 */ 121 public final int color; 122 /** 123 * @see UIProvider.ConversationColumns#ACCOUNT_URI 124 */ 125 public final Uri accountUri; 126 /** 127 * @see UIProvider.ConversationColumns#CONVERSATION_INFO 128 */ 129 public final ConversationInfo conversationInfo; 130 /** 131 * @see UIProvider.ConversationColumns#CONVERSATION_BASE_URI 132 */ 133 public final Uri conversationBaseUri; 134 /** 135 * @see UIProvider.ConversationColumns#REMOTE 136 */ 137 public final boolean isRemote; 138 /** 139 * @see UIProvider.ConversationColumns#ORDER_KEY 140 */ 141 public final long orderKey; 142 143 /** 144 * Used within the UI to indicate the adapter position of this conversation 145 * 146 * @deprecated Keeping this in sync with the desired value is a not always done properly, is a 147 * source of bugs, and is a bad idea in general. Do not trust this value. Try to 148 * migrate code away from using it. 149 */ 150 @Deprecated 151 public transient int position; 152 // Used within the UI to indicate that a Conversation should be removed from 153 // the ConversationCursor when executing an update, e.g. the the 154 // Conversation is no longer in the ConversationList for the current folder, 155 // that is it's now in some other folder(s) 156 public transient boolean localDeleteOnUpdate; 157 158 private transient boolean viewed; 159 160 private static String sBadgeAndSubject; 161 162 // Constituents of convFlags below 163 // Flag indicating that the item has been deleted, but will continue being 164 // shown in the list Delete/Archive of a mostly-dead item will NOT propagate 165 // the delete/archive, but WILL remove the item from the cursor 166 public static final int FLAG_MOSTLY_DEAD = 1 << 0; 167 168 /** An immutable, empty conversation list */ 169 public static final Collection<Conversation> EMPTY = Collections.emptyList(); 170 171 @Override 172 public int describeContents() { 173 return 0; 174 } 175 176 @Override 177 public void writeToParcel(Parcel dest, int flags) { 178 dest.writeLong(id); 179 dest.writeParcelable(uri, flags); 180 dest.writeString(subject); 181 dest.writeLong(dateMs); 182 dest.writeInt(hasAttachments ? 1 : 0); 183 dest.writeParcelable(messageListUri, 0); 184 dest.writeInt(sendingState); 185 dest.writeInt(priority); 186 dest.writeInt(read ? 1 : 0); 187 dest.writeInt(seen ? 1 : 0); 188 dest.writeInt(starred ? 1 : 0); 189 dest.writeParcelable(rawFolders, 0); 190 dest.writeInt(convFlags); 191 dest.writeInt(personalLevel); 192 dest.writeInt(spam ? 1 : 0); 193 dest.writeInt(phishing ? 1 : 0); 194 dest.writeInt(muted ? 1 : 0); 195 dest.writeInt(color); 196 dest.writeParcelable(accountUri, 0); 197 dest.writeParcelable(conversationInfo, 0); 198 dest.writeParcelable(conversationBaseUri, 0); 199 dest.writeInt(isRemote ? 1 : 0); 200 dest.writeLong(orderKey); 201 } 202 203 private Conversation(Parcel in, ClassLoader loader) { 204 id = in.readLong(); 205 uri = in.readParcelable(null); 206 subject = in.readString(); 207 dateMs = in.readLong(); 208 hasAttachments = (in.readInt() != 0); 209 messageListUri = in.readParcelable(null); 210 sendingState = in.readInt(); 211 priority = in.readInt(); 212 read = (in.readInt() != 0); 213 seen = (in.readInt() != 0); 214 starred = (in.readInt() != 0); 215 rawFolders = in.readParcelable(loader); 216 convFlags = in.readInt(); 217 personalLevel = in.readInt(); 218 spam = in.readInt() != 0; 219 phishing = in.readInt() != 0; 220 muted = in.readInt() != 0; 221 color = in.readInt(); 222 accountUri = in.readParcelable(null); 223 position = NO_POSITION; 224 localDeleteOnUpdate = false; 225 conversationInfo = in.readParcelable(loader); 226 conversationBaseUri = in.readParcelable(null); 227 isRemote = in.readInt() != 0; 228 orderKey = in.readLong(); 229 } 230 231 @Override 232 public String toString() { 233 // log extra info at DEBUG level or finer 234 final StringBuilder sb = new StringBuilder("[conversation id="); 235 sb.append(id); 236 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) { 237 sb.append(", subject="); 238 sb.append(subject); 239 } 240 sb.append("]"); 241 return sb.toString(); 242 } 243 244 public static final ClassLoaderCreator<Conversation> CREATOR = 245 new ClassLoaderCreator<Conversation>() { 246 247 @Override 248 public Conversation createFromParcel(Parcel source) { 249 return new Conversation(source, null); 250 } 251 252 @Override 253 public Conversation createFromParcel(Parcel source, ClassLoader loader) { 254 return new Conversation(source, loader); 255 } 256 257 @Override 258 public Conversation[] newArray(int size) { 259 return new Conversation[size]; 260 } 261 262 }; 263 264 public static final Uri MOVE_CONVERSATIONS_URI = Uri.parse("content://moveconversations"); 265 266 /** 267 * The column that needs to be updated to change the folders for a conversation. 268 */ 269 public static final String UPDATE_FOLDER_COLUMN = ConversationColumns.RAW_FOLDERS; 270 271 public Conversation(Cursor cursor) { 272 if (cursor == null) { 273 throw new IllegalArgumentException("Creating conversation from null cursor"); 274 } 275 id = cursor.getLong(UIProvider.CONVERSATION_ID_COLUMN); 276 uri = Uri.parse(cursor.getString(UIProvider.CONVERSATION_URI_COLUMN)); 277 dateMs = cursor.getLong(UIProvider.CONVERSATION_DATE_RECEIVED_MS_COLUMN); 278 final String subj = cursor.getString(UIProvider.CONVERSATION_SUBJECT_COLUMN); 279 // Don't allow null subject 280 if (subj == null) { 281 subject = ""; 282 } else { 283 subject = subj; 284 } 285 hasAttachments = cursor.getInt(UIProvider.CONVERSATION_HAS_ATTACHMENTS_COLUMN) != 0; 286 String messageList = cursor.getString(UIProvider.CONVERSATION_MESSAGE_LIST_URI_COLUMN); 287 messageListUri = !TextUtils.isEmpty(messageList) ? Uri.parse(messageList) : null; 288 sendingState = cursor.getInt(UIProvider.CONVERSATION_SENDING_STATE_COLUMN); 289 priority = cursor.getInt(UIProvider.CONVERSATION_PRIORITY_COLUMN); 290 read = cursor.getInt(UIProvider.CONVERSATION_READ_COLUMN) != 0; 291 seen = cursor.getInt(UIProvider.CONVERSATION_SEEN_COLUMN) != 0; 292 starred = cursor.getInt(UIProvider.CONVERSATION_STARRED_COLUMN) != 0; 293 rawFolders = readRawFolders(cursor); 294 convFlags = cursor.getInt(UIProvider.CONVERSATION_FLAGS_COLUMN); 295 personalLevel = cursor.getInt(UIProvider.CONVERSATION_PERSONAL_LEVEL_COLUMN); 296 spam = cursor.getInt(UIProvider.CONVERSATION_IS_SPAM_COLUMN) != 0; 297 phishing = cursor.getInt(UIProvider.CONVERSATION_IS_PHISHING_COLUMN) != 0; 298 muted = cursor.getInt(UIProvider.CONVERSATION_MUTED_COLUMN) != 0; 299 color = cursor.getInt(UIProvider.CONVERSATION_COLOR_COLUMN); 300 String account = cursor.getString(UIProvider.CONVERSATION_ACCOUNT_URI_COLUMN); 301 accountUri = !TextUtils.isEmpty(account) ? Uri.parse(account) : null; 302 position = NO_POSITION; 303 localDeleteOnUpdate = false; 304 conversationInfo = readConversationInfo(cursor); 305 if (conversationInfo == null) { 306 LogUtils.wtf(LOG_TAG, "Null conversation info from cursor"); 307 } 308 final String conversationBase = 309 cursor.getString(UIProvider.CONVERSATION_BASE_URI_COLUMN); 310 conversationBaseUri = !TextUtils.isEmpty(conversationBase) ? 311 Uri.parse(conversationBase) : null; 312 isRemote = cursor.getInt(UIProvider.CONVERSATION_REMOTE_COLUMN) != 0; 313 orderKey = cursor.getLong(UIProvider.CONVERSATION_ORDER_KEY_COLUMN); 314 } 315 316 public Conversation(Conversation other) { 317 if (other == null) { 318 throw new IllegalArgumentException("Copying null conversation"); 319 } 320 321 id = other.id; 322 uri = other.uri; 323 dateMs = other.dateMs; 324 subject = other.subject; 325 hasAttachments = other.hasAttachments; 326 messageListUri = other.messageListUri; 327 sendingState = other.sendingState; 328 priority = other.priority; 329 read = other.read; 330 seen = other.seen; 331 starred = other.starred; 332 rawFolders = other.rawFolders; // FolderList is immutable, shallow copy is OK 333 convFlags = other.convFlags; 334 personalLevel = other.personalLevel; 335 spam = other.spam; 336 phishing = other.phishing; 337 muted = other.muted; 338 color = other.color; 339 accountUri = other.accountUri; 340 position = other.position; 341 localDeleteOnUpdate = other.localDeleteOnUpdate; 342 // although ConversationInfo is mutable (see ConversationInfo.markRead), applyCachedValues 343 // will overwrite this if cached changes exist anyway, so a shallow copy is OK 344 conversationInfo = other.conversationInfo; 345 conversationBaseUri = other.conversationBaseUri; 346 isRemote = other.isRemote; 347 orderKey = other.orderKey; 348 } 349 350 private Conversation(long id, Uri uri, String subject, long dateMs, 351 boolean hasAttachment, Uri messageListUri, 352 int sendingState, int priority, boolean read, 353 boolean seen, boolean starred, FolderList rawFolders, int convFlags, int personalLevel, 354 boolean spam, boolean phishing, boolean muted, Uri accountUri, 355 ConversationInfo conversationInfo, Uri conversationBase, boolean isRemote, 356 String permalink, long orderKey) { 357 if (conversationInfo == null) { 358 throw new IllegalArgumentException("Null conversationInfo"); 359 } 360 this.id = id; 361 this.uri = uri; 362 this.subject = subject; 363 this.dateMs = dateMs; 364 this.hasAttachments = hasAttachment; 365 this.messageListUri = messageListUri; 366 this.sendingState = sendingState; 367 this.priority = priority; 368 this.read = read; 369 this.seen = seen; 370 this.starred = starred; 371 this.rawFolders = rawFolders; 372 this.convFlags = convFlags; 373 this.personalLevel = personalLevel; 374 this.spam = spam; 375 this.phishing = phishing; 376 this.muted = muted; 377 this.color = 0; 378 this.accountUri = accountUri; 379 this.conversationInfo = conversationInfo; 380 this.conversationBaseUri = conversationBase; 381 this.isRemote = isRemote; 382 this.orderKey = orderKey; 383 } 384 385 public static class Builder { 386 private long mId; 387 private Uri mUri; 388 private String mSubject; 389 private long mDateMs; 390 private boolean mHasAttachments; 391 private Uri mMessageListUri; 392 private int mSendingState; 393 private int mPriority; 394 private boolean mRead; 395 private boolean mSeen; 396 private boolean mStarred; 397 private FolderList mRawFolders; 398 private int mConvFlags; 399 private int mPersonalLevel; 400 private boolean mSpam; 401 private boolean mPhishing; 402 private boolean mMuted; 403 private Uri mAccountUri; 404 private ConversationInfo mConversationInfo; 405 private Uri mConversationBaseUri; 406 private boolean mIsRemote; 407 private String mPermalink; 408 private long mOrderKey; 409 410 public Builder setId(long id) { 411 mId = id; 412 return this; 413 } 414 415 public Builder setUri(Uri uri) { 416 mUri = uri; 417 return this; 418 } 419 420 public Builder setSubject(String subject) { 421 mSubject = subject; 422 return this; 423 } 424 425 public Builder setDateMs(long dateMs) { 426 mDateMs = dateMs; 427 return this; 428 } 429 430 public Builder setHasAttachments(boolean hasAttachments) { 431 mHasAttachments = hasAttachments; 432 return this; 433 } 434 435 public Builder setMessageListUri(Uri messageListUri) { 436 mMessageListUri = messageListUri; 437 return this; 438 } 439 440 public Builder setSendingState(int sendingState) { 441 mSendingState = sendingState; 442 return this; 443 } 444 445 public Builder setPriority(int priority) { 446 mPriority = priority; 447 return this; 448 } 449 450 public Builder setRead(boolean read) { 451 mRead = read; 452 return this; 453 } 454 455 public Builder setSeen(boolean seen) { 456 mSeen = seen; 457 return this; 458 } 459 460 public Builder setStarred(boolean starred) { 461 mStarred = starred; 462 return this; 463 } 464 465 public Builder setRawFolders(FolderList rawFolders) { 466 mRawFolders = rawFolders; 467 return this; 468 } 469 470 public Builder setConvFlags(int convFlags) { 471 mConvFlags = convFlags; 472 return this; 473 } 474 475 public Builder setPersonalLevel(int personalLevel) { 476 mPersonalLevel = personalLevel; 477 return this; 478 } 479 480 public Builder setSpam(boolean spam) { 481 mSpam = spam; 482 return this; 483 } 484 485 public Builder setPhishing(boolean phishing) { 486 mPhishing = phishing; 487 return this; 488 } 489 490 public Builder setMuted(boolean muted) { 491 mMuted = muted; 492 return this; 493 } 494 495 public Builder setAccountUri(Uri accountUri) { 496 mAccountUri = accountUri; 497 return this; 498 } 499 500 public Builder setConversationInfo(ConversationInfo conversationInfo) { 501 if (conversationInfo == null) { 502 throw new IllegalArgumentException("Can't set null ConversationInfo"); 503 } 504 mConversationInfo = conversationInfo; 505 return this; 506 } 507 508 public Builder setConversationBaseUri(Uri conversationBaseUri) { 509 mConversationBaseUri = conversationBaseUri; 510 return this; 511 } 512 513 public Builder setIsRemote(boolean isRemote) { 514 mIsRemote = isRemote; 515 return this; 516 } 517 518 public Builder setPermalink(String permalink) { 519 mPermalink = permalink; 520 return this; 521 } 522 523 public Builder setOrderKey(long orderKey) { 524 mOrderKey = orderKey; 525 return this; 526 } 527 528 public Builder() {} 529 530 public Conversation build() { 531 if (mConversationInfo == null) { 532 LogUtils.d(LOG_TAG, "Null conversationInfo in Builder"); 533 mConversationInfo = new ConversationInfo(); 534 } 535 return new Conversation(mId, mUri, mSubject, mDateMs, mHasAttachments, mMessageListUri, 536 mSendingState, mPriority, mRead, mSeen, mStarred, mRawFolders, mConvFlags, 537 mPersonalLevel, mSpam, mPhishing, mMuted, mAccountUri, mConversationInfo, 538 mConversationBaseUri, mIsRemote, mPermalink, mOrderKey); 539 } 540 } 541 542 private static final Bundle CONVERSATION_INFO_REQUEST; 543 private static final Bundle RAW_FOLDERS_REQUEST; 544 545 static { 546 RAW_FOLDERS_REQUEST = new Bundle(2); 547 RAW_FOLDERS_REQUEST.putBoolean( 548 ConversationCursorCommand.COMMAND_GET_RAW_FOLDERS, true); 549 RAW_FOLDERS_REQUEST.putInt( 550 ConversationCursorCommand.COMMAND_KEY_OPTIONS, 551 ConversationCursorCommand.OPTION_MOVE_POSITION); 552 553 CONVERSATION_INFO_REQUEST = new Bundle(2); 554 CONVERSATION_INFO_REQUEST.putBoolean( 555 ConversationCursorCommand.COMMAND_GET_CONVERSATION_INFO, true); 556 CONVERSATION_INFO_REQUEST.putInt( 557 ConversationCursorCommand.COMMAND_KEY_OPTIONS, 558 ConversationCursorCommand.OPTION_MOVE_POSITION); 559 } 560 561 private static ConversationInfo readConversationInfo(Cursor cursor) { 562 final ConversationInfo ci; 563 564 if (cursor instanceof ConversationCursor) { 565 final byte[] blob = ((ConversationCursor) cursor).getCachedBlob( 566 UIProvider.CONVERSATION_INFO_COLUMN); 567 if (blob != null && blob.length > 0) { 568 return ConversationInfo.fromBlob(blob); 569 } 570 } 571 572 final Bundle response = cursor.respond(CONVERSATION_INFO_REQUEST); 573 if (response.containsKey(ConversationCursorCommand.COMMAND_GET_CONVERSATION_INFO)) { 574 ci = response.getParcelable(ConversationCursorCommand.COMMAND_GET_CONVERSATION_INFO); 575 } else { 576 // legacy fallback 577 ci = ConversationInfo.fromBlob(cursor.getBlob(UIProvider.CONVERSATION_INFO_COLUMN)); 578 } 579 return ci; 580 } 581 582 private static FolderList readRawFolders(Cursor cursor) { 583 final FolderList fl; 584 585 if (cursor instanceof ConversationCursor) { 586 final byte[] blob = ((ConversationCursor) cursor).getCachedBlob( 587 UIProvider.CONVERSATION_RAW_FOLDERS_COLUMN); 588 if (blob != null && blob.length > 0) { 589 return FolderList.fromBlob(blob); 590 } 591 } 592 593 final Bundle response = cursor.respond(RAW_FOLDERS_REQUEST); 594 if (response.containsKey(ConversationCursorCommand.COMMAND_GET_RAW_FOLDERS)) { 595 fl = response.getParcelable(ConversationCursorCommand.COMMAND_GET_RAW_FOLDERS); 596 } else { 597 // legacy fallback 598 // TODO: delete this once Email supports the respond call 599 fl = FolderList.fromBlob( 600 cursor.getBlob(UIProvider.CONVERSATION_RAW_FOLDERS_COLUMN)); 601 } 602 return fl; 603 } 604 605 /** 606 * Apply any column values from the given {@link ContentValues} (where column names are the 607 * keys) to this conversation. 608 * 609 */ 610 public void applyCachedValues(ContentValues values) { 611 if (values == null) { 612 return; 613 } 614 for (String key : values.keySet()) { 615 final Object val = values.get(key); 616 LogUtils.i(LOG_TAG, "Conversation: applying cached value to col=%s val=%s", key, 617 val); 618 if (ConversationColumns.READ.equals(key)) { 619 read = (Integer) val != 0; 620 } else if (ConversationColumns.CONVERSATION_INFO.equals(key)) { 621 final ConversationInfo cachedCi = ConversationInfo.fromBlob((byte[]) val); 622 if (cachedCi == null) { 623 LogUtils.d(LOG_TAG, "Null ConversationInfo in applyCachedValues"); 624 } else { 625 conversationInfo.overwriteWith(cachedCi); 626 } 627 } else if (ConversationColumns.FLAGS.equals(key)) { 628 convFlags = (Integer) val; 629 } else if (ConversationColumns.STARRED.equals(key)) { 630 starred = (Integer) val != 0; 631 } else if (ConversationColumns.SEEN.equals(key)) { 632 seen = (Integer) val != 0; 633 } else if (ConversationColumns.RAW_FOLDERS.equals(key)) { 634 rawFolders = FolderList.fromBlob((byte[]) val); 635 } else if (ConversationColumns.VIEWED.equals(key)) { 636 // ignore. this is not read from the cursor, either. 637 } else if (ConversationColumns.PRIORITY.equals(key)) { 638 priority = (Integer) val; 639 } else { 640 LogUtils.e(LOG_TAG, new UnsupportedOperationException(), 641 "unsupported cached conv value in col=%s", key); 642 } 643 } 644 } 645 646 /** 647 * Get the <strong>immutable</strong> list of {@link Folder}s for this conversation. To modify 648 * this list, make a new {@link FolderList} and use {@link #setRawFolders(FolderList)}. 649 * 650 * @return <strong>Immutable</strong> list of {@link Folder}s. 651 */ 652 public List<Folder> getRawFolders() { 653 return rawFolders.folders; 654 } 655 656 public void setRawFolders(FolderList folders) { 657 rawFolders = folders; 658 } 659 660 @Override 661 public boolean equals(Object o) { 662 if (o instanceof Conversation) { 663 Conversation conv = (Conversation) o; 664 return conv.uri.equals(uri); 665 } 666 return false; 667 } 668 669 @Override 670 public int hashCode() { 671 return uri.hashCode(); 672 } 673 674 /** 675 * Get if this conversation is marked as high priority. 676 */ 677 public boolean isImportant() { 678 return priority == UIProvider.ConversationPriority.IMPORTANT; 679 } 680 681 /** 682 * Get if this conversation is mostly dead 683 */ 684 public boolean isMostlyDead() { 685 return (convFlags & FLAG_MOSTLY_DEAD) != 0; 686 } 687 688 /** 689 * Returns true if the URI of the conversation specified as the needle was 690 * found in the collection of conversations specified as the haystack. False 691 * otherwise. This method is safe to call with null arguments. 692 * 693 * @param haystack 694 * @param needle 695 * @return true if the needle was found in the haystack, false otherwise. 696 */ 697 public final static boolean contains(Collection<Conversation> haystack, Conversation needle) { 698 // If the haystack is empty, it cannot contain anything. 699 if (haystack == null || haystack.size() <= 0) { 700 return false; 701 } 702 // The null folder exists everywhere. 703 if (needle == null) { 704 return true; 705 } 706 final long toFind = needle.id; 707 for (final Conversation c : haystack) { 708 if (toFind == c.id) { 709 return true; 710 } 711 } 712 return false; 713 } 714 715 /** 716 * Returns a collection of a single conversation. This method always returns 717 * a valid collection even if the input conversation is null. 718 * 719 * @param in a conversation, possibly null. 720 * @return a collection of the conversation. 721 */ 722 public static Collection<Conversation> listOf(Conversation in) { 723 final Collection<Conversation> target = (in == null) ? EMPTY : ImmutableList.of(in); 724 return target; 725 } 726 727 /** 728 * Get the snippet for this conversation. 729 */ 730 public String getSnippet() { 731 return !TextUtils.isEmpty(conversationInfo.firstSnippet) ? 732 conversationInfo.firstSnippet : ""; 733 } 734 735 /** 736 * Get the number of messages for this conversation. 737 */ 738 public int getNumMessages() { 739 return conversationInfo.messageCount; 740 } 741 742 /** 743 * Get the number of drafts for this conversation. 744 */ 745 public int numDrafts() { 746 return conversationInfo.draftCount; 747 } 748 749 public boolean isViewed() { 750 return viewed; 751 } 752 753 public void markViewed() { 754 viewed = true; 755 } 756 757 public String getBaseUri(String defaultValue) { 758 return conversationBaseUri != null ? conversationBaseUri.toString() : defaultValue; 759 } 760 761 /** 762 * Create a human-readable string of all the conversations 763 * @param collection Any collection of conversations 764 * @return string with a human readable representation of the conversations. 765 */ 766 public static String toString(Collection<Conversation> collection) { 767 final StringBuilder out = new StringBuilder(collection.size() + " conversations:"); 768 int count = 0; 769 for (final Conversation c : collection) { 770 count++; 771 // Indent the conversations to make them easy to read in debug 772 // output. 773 out.append(" " + count + ": " + c.toString() + "\n"); 774 } 775 return out.toString(); 776 } 777 778 /** 779 * Returns an empty string if the specified string is null 780 */ 781 private static String emptyIfNull(String in) { 782 return in != null ? in : EMPTY_STRING; 783 } 784 785 /** 786 * Get the properly formatted badge and subject string for displaying a conversation. 787 */ 788 public static String getSubjectForDisplay(Context context, String badgeText, 789 String filteredSubject) { 790 if (TextUtils.isEmpty(filteredSubject)) { 791 return context.getString(R.string.no_subject); 792 } else if (!TextUtils.isEmpty(badgeText)) { 793 if (sBadgeAndSubject == null) { 794 sBadgeAndSubject = context.getString(R.string.badge_and_subject); 795 } 796 return String.format(sBadgeAndSubject, badgeText, filteredSubject); 797 } 798 799 return filteredSubject; 800 } 801 802 /** 803 * Public object that knows how to construct Conversation given Cursors. This is not used by 804 * {@link ConversationCursor} or {@link ConversationCursorLoader}. 805 */ 806 public static final CursorCreator<Conversation> FACTORY = new CursorCreator<Conversation>() { 807 @Override 808 public Conversation createFromCursor(final Cursor c) { 809 return new Conversation(c); 810 } 811 812 @Override 813 public String toString() { 814 return "Conversation CursorCreator"; 815 } 816 }; 817 } 818