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 describeContents()172 public int describeContents() { 173 return 0; 174 } 175 176 @Override writeToParcel(Parcel dest, int flags)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 Conversation(Parcel in, ClassLoader loader)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 toString()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 /** 265 * The column that needs to be updated to change the folders for a conversation. 266 */ 267 public static final String UPDATE_FOLDER_COLUMN = ConversationColumns.RAW_FOLDERS; 268 Conversation(Cursor cursor)269 public Conversation(Cursor cursor) { 270 if (cursor == null) { 271 throw new IllegalArgumentException("Creating conversation from null cursor"); 272 } 273 id = cursor.getLong(UIProvider.CONVERSATION_ID_COLUMN); 274 uri = Uri.parse(cursor.getString(UIProvider.CONVERSATION_URI_COLUMN)); 275 dateMs = cursor.getLong(UIProvider.CONVERSATION_DATE_RECEIVED_MS_COLUMN); 276 final String subj = cursor.getString(UIProvider.CONVERSATION_SUBJECT_COLUMN); 277 // Don't allow null subject 278 if (subj == null) { 279 subject = ""; 280 } else { 281 subject = subj; 282 } 283 hasAttachments = cursor.getInt(UIProvider.CONVERSATION_HAS_ATTACHMENTS_COLUMN) != 0; 284 String messageList = cursor.getString(UIProvider.CONVERSATION_MESSAGE_LIST_URI_COLUMN); 285 messageListUri = !TextUtils.isEmpty(messageList) ? Uri.parse(messageList) : null; 286 sendingState = cursor.getInt(UIProvider.CONVERSATION_SENDING_STATE_COLUMN); 287 priority = cursor.getInt(UIProvider.CONVERSATION_PRIORITY_COLUMN); 288 read = cursor.getInt(UIProvider.CONVERSATION_READ_COLUMN) != 0; 289 seen = cursor.getInt(UIProvider.CONVERSATION_SEEN_COLUMN) != 0; 290 starred = cursor.getInt(UIProvider.CONVERSATION_STARRED_COLUMN) != 0; 291 rawFolders = readRawFolders(cursor); 292 convFlags = cursor.getInt(UIProvider.CONVERSATION_FLAGS_COLUMN); 293 personalLevel = cursor.getInt(UIProvider.CONVERSATION_PERSONAL_LEVEL_COLUMN); 294 spam = cursor.getInt(UIProvider.CONVERSATION_IS_SPAM_COLUMN) != 0; 295 phishing = cursor.getInt(UIProvider.CONVERSATION_IS_PHISHING_COLUMN) != 0; 296 muted = cursor.getInt(UIProvider.CONVERSATION_MUTED_COLUMN) != 0; 297 color = cursor.getInt(UIProvider.CONVERSATION_COLOR_COLUMN); 298 String account = cursor.getString(UIProvider.CONVERSATION_ACCOUNT_URI_COLUMN); 299 accountUri = !TextUtils.isEmpty(account) ? Uri.parse(account) : null; 300 position = NO_POSITION; 301 localDeleteOnUpdate = false; 302 conversationInfo = readConversationInfo(cursor); 303 if (conversationInfo == null) { 304 LogUtils.wtf(LOG_TAG, "Null conversation info from cursor"); 305 } 306 final String conversationBase = 307 cursor.getString(UIProvider.CONVERSATION_BASE_URI_COLUMN); 308 conversationBaseUri = !TextUtils.isEmpty(conversationBase) ? 309 Uri.parse(conversationBase) : null; 310 isRemote = cursor.getInt(UIProvider.CONVERSATION_REMOTE_COLUMN) != 0; 311 orderKey = cursor.getLong(UIProvider.CONVERSATION_ORDER_KEY_COLUMN); 312 } 313 Conversation(Conversation other)314 public Conversation(Conversation other) { 315 if (other == null) { 316 throw new IllegalArgumentException("Copying null conversation"); 317 } 318 319 id = other.id; 320 uri = other.uri; 321 dateMs = other.dateMs; 322 subject = other.subject; 323 hasAttachments = other.hasAttachments; 324 messageListUri = other.messageListUri; 325 sendingState = other.sendingState; 326 priority = other.priority; 327 read = other.read; 328 seen = other.seen; 329 starred = other.starred; 330 rawFolders = other.rawFolders; // FolderList is immutable, shallow copy is OK 331 convFlags = other.convFlags; 332 personalLevel = other.personalLevel; 333 spam = other.spam; 334 phishing = other.phishing; 335 muted = other.muted; 336 color = other.color; 337 accountUri = other.accountUri; 338 position = other.position; 339 localDeleteOnUpdate = other.localDeleteOnUpdate; 340 // although ConversationInfo is mutable (see ConversationInfo.markRead), applyCachedValues 341 // will overwrite this if cached changes exist anyway, so a shallow copy is OK 342 conversationInfo = other.conversationInfo; 343 conversationBaseUri = other.conversationBaseUri; 344 isRemote = other.isRemote; 345 orderKey = other.orderKey; 346 } 347 Conversation(long id, Uri uri, String subject, long dateMs, boolean hasAttachment, Uri messageListUri, int sendingState, int priority, boolean read, boolean seen, boolean starred, FolderList rawFolders, int convFlags, int personalLevel, boolean spam, boolean phishing, boolean muted, Uri accountUri, ConversationInfo conversationInfo, Uri conversationBase, boolean isRemote, String permalink, long orderKey)348 private Conversation(long id, Uri uri, String subject, long dateMs, 349 boolean hasAttachment, Uri messageListUri, 350 int sendingState, int priority, boolean read, 351 boolean seen, boolean starred, FolderList rawFolders, int convFlags, int personalLevel, 352 boolean spam, boolean phishing, boolean muted, Uri accountUri, 353 ConversationInfo conversationInfo, Uri conversationBase, boolean isRemote, 354 String permalink, long orderKey) { 355 if (conversationInfo == null) { 356 throw new IllegalArgumentException("Null conversationInfo"); 357 } 358 this.id = id; 359 this.uri = uri; 360 this.subject = subject; 361 this.dateMs = dateMs; 362 this.hasAttachments = hasAttachment; 363 this.messageListUri = messageListUri; 364 this.sendingState = sendingState; 365 this.priority = priority; 366 this.read = read; 367 this.seen = seen; 368 this.starred = starred; 369 this.rawFolders = rawFolders; 370 this.convFlags = convFlags; 371 this.personalLevel = personalLevel; 372 this.spam = spam; 373 this.phishing = phishing; 374 this.muted = muted; 375 this.color = 0; 376 this.accountUri = accountUri; 377 this.conversationInfo = conversationInfo; 378 this.conversationBaseUri = conversationBase; 379 this.isRemote = isRemote; 380 this.orderKey = orderKey; 381 } 382 383 public static class Builder { 384 private long mId; 385 private Uri mUri; 386 private String mSubject; 387 private long mDateMs; 388 private boolean mHasAttachments; 389 private Uri mMessageListUri; 390 private int mSendingState; 391 private int mPriority; 392 private boolean mRead; 393 private boolean mSeen; 394 private boolean mStarred; 395 private FolderList mRawFolders; 396 private int mConvFlags; 397 private int mPersonalLevel; 398 private boolean mSpam; 399 private boolean mPhishing; 400 private boolean mMuted; 401 private Uri mAccountUri; 402 private ConversationInfo mConversationInfo; 403 private Uri mConversationBaseUri; 404 private boolean mIsRemote; 405 private String mPermalink; 406 private long mOrderKey; 407 setId(long id)408 public Builder setId(long id) { 409 mId = id; 410 return this; 411 } 412 setUri(Uri uri)413 public Builder setUri(Uri uri) { 414 mUri = uri; 415 return this; 416 } 417 setSubject(String subject)418 public Builder setSubject(String subject) { 419 mSubject = subject; 420 return this; 421 } 422 setDateMs(long dateMs)423 public Builder setDateMs(long dateMs) { 424 mDateMs = dateMs; 425 return this; 426 } 427 setHasAttachments(boolean hasAttachments)428 public Builder setHasAttachments(boolean hasAttachments) { 429 mHasAttachments = hasAttachments; 430 return this; 431 } 432 setMessageListUri(Uri messageListUri)433 public Builder setMessageListUri(Uri messageListUri) { 434 mMessageListUri = messageListUri; 435 return this; 436 } 437 setSendingState(int sendingState)438 public Builder setSendingState(int sendingState) { 439 mSendingState = sendingState; 440 return this; 441 } 442 setPriority(int priority)443 public Builder setPriority(int priority) { 444 mPriority = priority; 445 return this; 446 } 447 setRead(boolean read)448 public Builder setRead(boolean read) { 449 mRead = read; 450 return this; 451 } 452 setSeen(boolean seen)453 public Builder setSeen(boolean seen) { 454 mSeen = seen; 455 return this; 456 } 457 setStarred(boolean starred)458 public Builder setStarred(boolean starred) { 459 mStarred = starred; 460 return this; 461 } 462 setRawFolders(FolderList rawFolders)463 public Builder setRawFolders(FolderList rawFolders) { 464 mRawFolders = rawFolders; 465 return this; 466 } 467 setConvFlags(int convFlags)468 public Builder setConvFlags(int convFlags) { 469 mConvFlags = convFlags; 470 return this; 471 } 472 setPersonalLevel(int personalLevel)473 public Builder setPersonalLevel(int personalLevel) { 474 mPersonalLevel = personalLevel; 475 return this; 476 } 477 setSpam(boolean spam)478 public Builder setSpam(boolean spam) { 479 mSpam = spam; 480 return this; 481 } 482 setPhishing(boolean phishing)483 public Builder setPhishing(boolean phishing) { 484 mPhishing = phishing; 485 return this; 486 } 487 setMuted(boolean muted)488 public Builder setMuted(boolean muted) { 489 mMuted = muted; 490 return this; 491 } 492 setAccountUri(Uri accountUri)493 public Builder setAccountUri(Uri accountUri) { 494 mAccountUri = accountUri; 495 return this; 496 } 497 setConversationInfo(ConversationInfo conversationInfo)498 public Builder setConversationInfo(ConversationInfo conversationInfo) { 499 if (conversationInfo == null) { 500 throw new IllegalArgumentException("Can't set null ConversationInfo"); 501 } 502 mConversationInfo = conversationInfo; 503 return this; 504 } 505 setConversationBaseUri(Uri conversationBaseUri)506 public Builder setConversationBaseUri(Uri conversationBaseUri) { 507 mConversationBaseUri = conversationBaseUri; 508 return this; 509 } 510 setIsRemote(boolean isRemote)511 public Builder setIsRemote(boolean isRemote) { 512 mIsRemote = isRemote; 513 return this; 514 } 515 setPermalink(String permalink)516 public Builder setPermalink(String permalink) { 517 mPermalink = permalink; 518 return this; 519 } 520 setOrderKey(long orderKey)521 public Builder setOrderKey(long orderKey) { 522 mOrderKey = orderKey; 523 return this; 524 } 525 Builder()526 public Builder() {} 527 build()528 public Conversation build() { 529 if (mConversationInfo == null) { 530 LogUtils.d(LOG_TAG, "Null conversationInfo in Builder"); 531 mConversationInfo = new ConversationInfo(); 532 } 533 return new Conversation(mId, mUri, mSubject, mDateMs, mHasAttachments, mMessageListUri, 534 mSendingState, mPriority, mRead, mSeen, mStarred, mRawFolders, mConvFlags, 535 mPersonalLevel, mSpam, mPhishing, mMuted, mAccountUri, mConversationInfo, 536 mConversationBaseUri, mIsRemote, mPermalink, mOrderKey); 537 } 538 } 539 540 private static final Bundle CONVERSATION_INFO_REQUEST; 541 private static final Bundle RAW_FOLDERS_REQUEST; 542 543 static { 544 RAW_FOLDERS_REQUEST = new Bundle(2); RAW_FOLDERS_REQUEST.putBoolean( ConversationCursorCommand.COMMAND_GET_RAW_FOLDERS, true)545 RAW_FOLDERS_REQUEST.putBoolean( 546 ConversationCursorCommand.COMMAND_GET_RAW_FOLDERS, true); RAW_FOLDERS_REQUEST.putInt( ConversationCursorCommand.COMMAND_KEY_OPTIONS, ConversationCursorCommand.OPTION_MOVE_POSITION)547 RAW_FOLDERS_REQUEST.putInt( 548 ConversationCursorCommand.COMMAND_KEY_OPTIONS, 549 ConversationCursorCommand.OPTION_MOVE_POSITION); 550 551 CONVERSATION_INFO_REQUEST = new Bundle(2); CONVERSATION_INFO_REQUEST.putBoolean( ConversationCursorCommand.COMMAND_GET_CONVERSATION_INFO, true)552 CONVERSATION_INFO_REQUEST.putBoolean( 553 ConversationCursorCommand.COMMAND_GET_CONVERSATION_INFO, true); CONVERSATION_INFO_REQUEST.putInt( ConversationCursorCommand.COMMAND_KEY_OPTIONS, ConversationCursorCommand.OPTION_MOVE_POSITION)554 CONVERSATION_INFO_REQUEST.putInt( 555 ConversationCursorCommand.COMMAND_KEY_OPTIONS, 556 ConversationCursorCommand.OPTION_MOVE_POSITION); 557 } 558 readConversationInfo(Cursor cursor)559 private static ConversationInfo readConversationInfo(Cursor cursor) { 560 final ConversationInfo ci; 561 562 if (cursor instanceof ConversationCursor) { 563 final byte[] blob = ((ConversationCursor) cursor).getCachedBlob( 564 UIProvider.CONVERSATION_INFO_COLUMN); 565 if (blob != null && blob.length > 0) { 566 return ConversationInfo.fromBlob(blob); 567 } 568 } 569 570 final Bundle response = cursor.respond(CONVERSATION_INFO_REQUEST); 571 if (response.containsKey(ConversationCursorCommand.COMMAND_GET_CONVERSATION_INFO)) { 572 ci = response.getParcelable(ConversationCursorCommand.COMMAND_GET_CONVERSATION_INFO); 573 } else { 574 // legacy fallback 575 ci = ConversationInfo.fromBlob(cursor.getBlob(UIProvider.CONVERSATION_INFO_COLUMN)); 576 } 577 return ci; 578 } 579 readRawFolders(Cursor cursor)580 private static FolderList readRawFolders(Cursor cursor) { 581 final FolderList fl; 582 583 if (cursor instanceof ConversationCursor) { 584 final byte[] blob = ((ConversationCursor) cursor).getCachedBlob( 585 UIProvider.CONVERSATION_RAW_FOLDERS_COLUMN); 586 if (blob != null && blob.length > 0) { 587 return FolderList.fromBlob(blob); 588 } 589 } 590 591 final Bundle response = cursor.respond(RAW_FOLDERS_REQUEST); 592 if (response.containsKey(ConversationCursorCommand.COMMAND_GET_RAW_FOLDERS)) { 593 fl = response.getParcelable(ConversationCursorCommand.COMMAND_GET_RAW_FOLDERS); 594 } else { 595 // legacy fallback 596 // TODO: delete this once Email supports the respond call 597 fl = FolderList.fromBlob( 598 cursor.getBlob(UIProvider.CONVERSATION_RAW_FOLDERS_COLUMN)); 599 } 600 return fl; 601 } 602 603 /** 604 * Apply any column values from the given {@link ContentValues} (where column names are the 605 * keys) to this conversation. 606 * 607 */ applyCachedValues(ContentValues values)608 public void applyCachedValues(ContentValues values) { 609 if (values == null) { 610 return; 611 } 612 for (String key : values.keySet()) { 613 final Object val = values.get(key); 614 LogUtils.i(LOG_TAG, "Conversation: applying cached value to col=%s val=%s", key, 615 val); 616 if (ConversationColumns.READ.equals(key)) { 617 read = (Integer) val != 0; 618 } else if (ConversationColumns.CONVERSATION_INFO.equals(key)) { 619 final ConversationInfo cachedCi = ConversationInfo.fromBlob((byte[]) val); 620 if (cachedCi == null) { 621 LogUtils.d(LOG_TAG, "Null ConversationInfo in applyCachedValues"); 622 } else { 623 conversationInfo.overwriteWith(cachedCi); 624 } 625 } else if (ConversationColumns.FLAGS.equals(key)) { 626 convFlags = (Integer) val; 627 } else if (ConversationColumns.STARRED.equals(key)) { 628 starred = (Integer) val != 0; 629 } else if (ConversationColumns.SEEN.equals(key)) { 630 seen = (Integer) val != 0; 631 } else if (ConversationColumns.RAW_FOLDERS.equals(key)) { 632 rawFolders = FolderList.fromBlob((byte[]) val); 633 } else if (ConversationColumns.VIEWED.equals(key)) { 634 // ignore. this is not read from the cursor, either. 635 } else if (ConversationColumns.PRIORITY.equals(key)) { 636 priority = (Integer) val; 637 } else { 638 LogUtils.e(LOG_TAG, new UnsupportedOperationException(), 639 "unsupported cached conv value in col=%s", key); 640 } 641 } 642 } 643 644 /** 645 * Get the <strong>immutable</strong> list of {@link Folder}s for this conversation. To modify 646 * this list, make a new {@link FolderList} and use {@link #setRawFolders(FolderList)}. 647 * 648 * @return <strong>Immutable</strong> list of {@link Folder}s. 649 */ getRawFolders()650 public List<Folder> getRawFolders() { 651 return rawFolders.folders; 652 } 653 setRawFolders(FolderList folders)654 public void setRawFolders(FolderList folders) { 655 rawFolders = folders; 656 } 657 658 @Override equals(Object o)659 public boolean equals(Object o) { 660 if (o instanceof Conversation) { 661 Conversation conv = (Conversation) o; 662 return conv.uri.equals(uri); 663 } 664 return false; 665 } 666 667 @Override hashCode()668 public int hashCode() { 669 return uri.hashCode(); 670 } 671 672 /** 673 * Get if this conversation is marked as high priority. 674 */ isImportant()675 public boolean isImportant() { 676 return priority == UIProvider.ConversationPriority.IMPORTANT; 677 } 678 679 /** 680 * Get if this conversation is mostly dead 681 */ isMostlyDead()682 public boolean isMostlyDead() { 683 return (convFlags & FLAG_MOSTLY_DEAD) != 0; 684 } 685 686 /** 687 * Returns true if the URI of the conversation specified as the needle was 688 * found in the collection of conversations specified as the haystack. False 689 * otherwise. This method is safe to call with null arguments. 690 * 691 * @param haystack 692 * @param needle 693 * @return true if the needle was found in the haystack, false otherwise. 694 */ contains(Collection<Conversation> haystack, Conversation needle)695 public final static boolean contains(Collection<Conversation> haystack, Conversation needle) { 696 // If the haystack is empty, it cannot contain anything. 697 if (haystack == null || haystack.size() <= 0) { 698 return false; 699 } 700 // The null folder exists everywhere. 701 if (needle == null) { 702 return true; 703 } 704 final long toFind = needle.id; 705 for (final Conversation c : haystack) { 706 if (toFind == c.id) { 707 return true; 708 } 709 } 710 return false; 711 } 712 713 /** 714 * Returns a collection of a single conversation. This method always returns 715 * a valid collection even if the input conversation is null. 716 * 717 * @param in a conversation, possibly null. 718 * @return a collection of the conversation. 719 */ listOf(Conversation in)720 public static Collection<Conversation> listOf(Conversation in) { 721 final Collection<Conversation> target = (in == null) ? EMPTY : ImmutableList.of(in); 722 return target; 723 } 724 725 /** 726 * Get the snippet for this conversation. 727 */ getSnippet()728 public String getSnippet() { 729 return !TextUtils.isEmpty(conversationInfo.firstSnippet) ? 730 conversationInfo.firstSnippet : ""; 731 } 732 733 /** 734 * Get the number of messages for this conversation. 735 */ getNumMessages()736 public int getNumMessages() { 737 return conversationInfo.messageCount; 738 } 739 740 /** 741 * Get the number of drafts for this conversation. 742 */ numDrafts()743 public int numDrafts() { 744 return conversationInfo.draftCount; 745 } 746 isViewed()747 public boolean isViewed() { 748 return viewed; 749 } 750 markViewed()751 public void markViewed() { 752 viewed = true; 753 } 754 getBaseUri(String defaultValue)755 public String getBaseUri(String defaultValue) { 756 return conversationBaseUri != null ? conversationBaseUri.toString() : defaultValue; 757 } 758 759 /** 760 * Returns {@code true} if the conversation is in the trash folder. 761 */ isInTrash()762 public boolean isInTrash() { 763 for (Folder folder : getRawFolders()) { 764 if (folder.isTrash()) { 765 return true; 766 } 767 } 768 769 return false; 770 } 771 772 /** 773 * Create a human-readable string of all the conversations 774 * @param collection Any collection of conversations 775 * @return string with a human readable representation of the conversations. 776 */ toString(Collection<Conversation> collection)777 public static String toString(Collection<Conversation> collection) { 778 final StringBuilder out = new StringBuilder(collection.size() + " conversations:"); 779 int count = 0; 780 for (final Conversation c : collection) { 781 count++; 782 // Indent the conversations to make them easy to read in debug 783 // output. 784 out.append(" " + count + ": " + c.toString() + "\n"); 785 } 786 return out.toString(); 787 } 788 789 /** 790 * Returns an empty string if the specified string is null 791 */ emptyIfNull(String in)792 private static String emptyIfNull(String in) { 793 return in != null ? in : EMPTY_STRING; 794 } 795 796 /** 797 * Get the properly formatted badge and subject string for displaying a conversation. 798 */ getSubjectForDisplay(Context context, String badgeText, String filteredSubject)799 public static String getSubjectForDisplay(Context context, String badgeText, 800 String filteredSubject) { 801 if (TextUtils.isEmpty(filteredSubject)) { 802 return context.getString(R.string.no_subject); 803 } else if (!TextUtils.isEmpty(badgeText)) { 804 if (sBadgeAndSubject == null) { 805 sBadgeAndSubject = context.getString(R.string.badge_and_subject); 806 } 807 return String.format(sBadgeAndSubject, badgeText, filteredSubject); 808 } 809 810 return filteredSubject; 811 } 812 813 /** 814 * Public object that knows how to construct Conversation given Cursors. This is not used by 815 * {@link ConversationCursor} or {@link ConversationCursorLoader}. 816 */ 817 public static final CursorCreator<Conversation> FACTORY = new CursorCreator<Conversation>() { 818 @Override 819 public Conversation createFromCursor(final Cursor c) { 820 return new Conversation(c); 821 } 822 823 @Override 824 public String toString() { 825 return "Conversation CursorCreator"; 826 } 827 }; 828 } 829