Conversation.java revision dbb587f15c723ae80edb33f65c29cc2b6c15eba0
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 17package com.android.mail.providers; 18 19import android.content.ContentValues; 20import android.content.Context; 21import android.database.Cursor; 22import android.net.Uri; 23import android.os.Parcel; 24import android.os.Parcelable; 25import android.provider.BaseColumns; 26import android.text.TextUtils; 27 28import com.android.mail.R; 29import com.android.mail.providers.UIProvider.ConversationColumns; 30import com.android.mail.utils.LogTag; 31import com.android.mail.utils.LogUtils; 32import com.google.common.collect.ImmutableList; 33import com.google.common.collect.Lists; 34 35import java.util.ArrayList; 36import java.util.Collection; 37import java.util.Collections; 38import java.util.List; 39 40public class Conversation implements Parcelable { 41 public static final int NO_POSITION = -1; 42 43 private static final String LOG_TAG = LogTag.getLogTag(); 44 45 private static final String EMPTY_STRING = ""; 46 47 /** 48 * @see BaseColumns#_ID 49 */ 50 public long id; 51 /** 52 * @see UIProvider.ConversationColumns#URI 53 */ 54 public Uri uri; 55 /** 56 * @see UIProvider.ConversationColumns#SUBJECT 57 */ 58 public String subject; 59 /** 60 * @see UIProvider.ConversationColumns#DATE_RECEIVED_MS 61 */ 62 public long dateMs; 63 /** 64 * @see UIProvider.ConversationColumns#SNIPPET 65 */ 66 @Deprecated 67 public String snippet; 68 /** 69 * @see UIProvider.ConversationColumns#HAS_ATTACHMENTS 70 */ 71 public boolean hasAttachments; 72 /** 73 * @see UIProvider.ConversationColumns#MESSAGE_LIST_URI 74 */ 75 public Uri messageListUri; 76 /** 77 * @see UIProvider.ConversationColumns#SENDER_INFO 78 */ 79 @Deprecated 80 public String senders; 81 /** 82 * @see UIProvider.ConversationColumns#NUM_MESSAGES 83 */ 84 private int numMessages; 85 /** 86 * @see UIProvider.ConversationColumns#NUM_DRAFTS 87 */ 88 private int numDrafts; 89 /** 90 * @see UIProvider.ConversationColumns#SENDING_STATE 91 */ 92 public int sendingState; 93 /** 94 * @see UIProvider.ConversationColumns#PRIORITY 95 */ 96 public int priority; 97 /** 98 * @see UIProvider.ConversationColumns#READ 99 */ 100 public boolean read; 101 /** 102 * @see UIProvider.ConversationColumns#SEEN 103 */ 104 public boolean seen; 105 /** 106 * @see UIProvider.ConversationColumns#STARRED 107 */ 108 public boolean starred; 109 /** 110 * @see UIProvider.ConversationColumns#RAW_FOLDERS 111 */ 112 private FolderList rawFolders; 113 /** 114 * @see UIProvider.ConversationColumns#FLAGS 115 */ 116 public int convFlags; 117 /** 118 * @see UIProvider.ConversationColumns#PERSONAL_LEVEL 119 */ 120 public int personalLevel; 121 /** 122 * @see UIProvider.ConversationColumns#SPAM 123 */ 124 public boolean spam; 125 /** 126 * @see UIProvider.ConversationColumns#MUTED 127 */ 128 public boolean muted; 129 /** 130 * @see UIProvider.ConversationColumns#PHISHING 131 */ 132 public boolean phishing; 133 /** 134 * @see UIProvider.ConversationColumns#COLOR 135 */ 136 public int color; 137 /** 138 * @see UIProvider.ConversationColumns#ACCOUNT_URI 139 */ 140 public Uri accountUri; 141 /** 142 * @see UIProvider.ConversationColumns#CONVERSATION_INFO 143 */ 144 public ConversationInfo conversationInfo; 145 /** 146 * @see UIProvider.ConversationColumns#CONVERSATION_BASE_URI 147 */ 148 public Uri conversationBaseUri; 149 /** 150 * @see UIProvider.ConversationColumns#REMOTE 151 */ 152 public boolean isRemote; 153 154 /** 155 * Used within the UI to indicate the adapter position of this conversation 156 * 157 * @deprecated Keeping this in sync with the desired value is a not always done properly, is a 158 * source of bugs, and is a bad idea in general. Do not trust this value. Try to 159 * migrate code away from using it. 160 */ 161 @Deprecated 162 public transient int position; 163 // Used within the UI to indicate that a Conversation should be removed from 164 // the ConversationCursor when executing an update, e.g. the the 165 // Conversation is no longer in the ConversationList for the current folder, 166 // that is it's now in some other folder(s) 167 public transient boolean localDeleteOnUpdate; 168 169 private transient boolean viewed; 170 171 private ArrayList<Folder> cachedDisplayableFolders; 172 173 private static String sSubjectAndSnippet; 174 175 // Constituents of convFlags below 176 // Flag indicating that the item has been deleted, but will continue being 177 // shown in the list Delete/Archive of a mostly-dead item will NOT propagate 178 // the delete/archive, but WILL remove the item from the cursor 179 public static final int FLAG_MOSTLY_DEAD = 1 << 0; 180 181 /** An immutable, empty conversation list */ 182 public static final Collection<Conversation> EMPTY = Collections.emptyList(); 183 184 @Override 185 public int describeContents() { 186 return 0; 187 } 188 189 @Override 190 public void writeToParcel(Parcel dest, int flags) { 191 dest.writeLong(id); 192 dest.writeParcelable(uri, flags); 193 dest.writeString(subject); 194 dest.writeLong(dateMs); 195 dest.writeString(snippet); 196 dest.writeInt(hasAttachments ? 1 : 0); 197 dest.writeParcelable(messageListUri, 0); 198 dest.writeString(senders); 199 dest.writeInt(numMessages); 200 dest.writeInt(numDrafts); 201 dest.writeInt(sendingState); 202 dest.writeInt(priority); 203 dest.writeInt(read ? 1 : 0); 204 dest.writeInt(seen ? 1 : 0); 205 dest.writeInt(starred ? 1 : 0); 206 dest.writeParcelable(rawFolders, 0); 207 dest.writeInt(convFlags); 208 dest.writeInt(personalLevel); 209 dest.writeInt(spam ? 1 : 0); 210 dest.writeInt(phishing ? 1 : 0); 211 dest.writeInt(muted ? 1 : 0); 212 dest.writeInt(color); 213 dest.writeParcelable(accountUri, 0); 214 dest.writeParcelable(conversationInfo, 0); 215 dest.writeParcelable(conversationBaseUri, 0); 216 dest.writeInt(isRemote ? 1 : 0); 217 } 218 219 private Conversation(Parcel in, ClassLoader loader) { 220 id = in.readLong(); 221 uri = in.readParcelable(null); 222 subject = in.readString(); 223 dateMs = in.readLong(); 224 snippet = in.readString(); 225 hasAttachments = (in.readInt() != 0); 226 messageListUri = in.readParcelable(null); 227 senders = emptyIfNull(in.readString()); 228 numMessages = in.readInt(); 229 numDrafts = in.readInt(); 230 sendingState = in.readInt(); 231 priority = in.readInt(); 232 read = (in.readInt() != 0); 233 seen = (in.readInt() != 0); 234 starred = (in.readInt() != 0); 235 rawFolders = in.readParcelable(loader); 236 convFlags = in.readInt(); 237 personalLevel = in.readInt(); 238 spam = in.readInt() != 0; 239 phishing = in.readInt() != 0; 240 muted = in.readInt() != 0; 241 color = in.readInt(); 242 accountUri = in.readParcelable(null); 243 position = NO_POSITION; 244 localDeleteOnUpdate = false; 245 conversationInfo = in.readParcelable(loader); 246 conversationBaseUri = in.readParcelable(null); 247 isRemote = in.readInt() != 0; 248 } 249 250 @Override 251 public String toString() { 252 // log extra info at DEBUG level or finer 253 final StringBuilder sb = new StringBuilder("[conversation id="); 254 sb.append(id); 255 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) { 256 sb.append(", subject="); 257 sb.append(subject); 258 } 259 sb.append("]"); 260 return sb.toString(); 261 } 262 263 public static final ClassLoaderCreator<Conversation> CREATOR = 264 new ClassLoaderCreator<Conversation>() { 265 266 @Override 267 public Conversation createFromParcel(Parcel source) { 268 return new Conversation(source, null); 269 } 270 271 @Override 272 public Conversation createFromParcel(Parcel source, ClassLoader loader) { 273 return new Conversation(source, loader); 274 } 275 276 @Override 277 public Conversation[] newArray(int size) { 278 return new Conversation[size]; 279 } 280 281 }; 282 283 public static final Uri MOVE_CONVERSATIONS_URI = Uri.parse("content://moveconversations"); 284 285 /** 286 * The column that needs to be updated to change the folders for a conversation. 287 */ 288 public static final String UPDATE_FOLDER_COLUMN = ConversationColumns.RAW_FOLDERS; 289 290 public Conversation(Cursor cursor) { 291 if (cursor != null) { 292 id = cursor.getLong(UIProvider.CONVERSATION_ID_COLUMN); 293 uri = Uri.parse(cursor.getString(UIProvider.CONVERSATION_URI_COLUMN)); 294 dateMs = cursor.getLong(UIProvider.CONVERSATION_DATE_RECEIVED_MS_COLUMN); 295 subject = cursor.getString(UIProvider.CONVERSATION_SUBJECT_COLUMN); 296 // Don't allow null subject 297 if (subject == null) { 298 subject = ""; 299 } 300 hasAttachments = cursor.getInt(UIProvider.CONVERSATION_HAS_ATTACHMENTS_COLUMN) != 0; 301 String messageList = cursor.getString(UIProvider.CONVERSATION_MESSAGE_LIST_URI_COLUMN); 302 messageListUri = !TextUtils.isEmpty(messageList) ? Uri.parse(messageList) : null; 303 sendingState = cursor.getInt(UIProvider.CONVERSATION_SENDING_STATE_COLUMN); 304 priority = cursor.getInt(UIProvider.CONVERSATION_PRIORITY_COLUMN); 305 read = cursor.getInt(UIProvider.CONVERSATION_READ_COLUMN) != 0; 306 seen = cursor.getInt(UIProvider.CONVERSATION_SEEN_COLUMN) != 0; 307 starred = cursor.getInt(UIProvider.CONVERSATION_STARRED_COLUMN) != 0; 308 rawFolders = FolderList.fromBlob( 309 cursor.getBlob(UIProvider.CONVERSATION_RAW_FOLDERS_COLUMN)); 310 convFlags = cursor.getInt(UIProvider.CONVERSATION_FLAGS_COLUMN); 311 personalLevel = cursor.getInt(UIProvider.CONVERSATION_PERSONAL_LEVEL_COLUMN); 312 spam = cursor.getInt(UIProvider.CONVERSATION_IS_SPAM_COLUMN) != 0; 313 phishing = cursor.getInt(UIProvider.CONVERSATION_IS_PHISHING_COLUMN) != 0; 314 muted = cursor.getInt(UIProvider.CONVERSATION_MUTED_COLUMN) != 0; 315 color = cursor.getInt(UIProvider.CONVERSATION_COLOR_COLUMN); 316 String account = cursor.getString(UIProvider.CONVERSATION_ACCOUNT_URI_COLUMN); 317 accountUri = !TextUtils.isEmpty(account) ? Uri.parse(account) : null; 318 position = NO_POSITION; 319 localDeleteOnUpdate = false; 320 conversationInfo = ConversationInfo.fromBlob( 321 cursor.getBlob(UIProvider.CONVERSATION_INFO_COLUMN)); 322 final String conversationBase = 323 cursor.getString(UIProvider.CONVERSATION_BASE_URI_COLUMN); 324 conversationBaseUri = !TextUtils.isEmpty(conversationBase) ? 325 Uri.parse(conversationBase) : null; 326 if (conversationInfo == null) { 327 snippet = cursor.getString(UIProvider.CONVERSATION_SNIPPET_COLUMN); 328 senders = emptyIfNull(cursor.getString(UIProvider.CONVERSATION_SENDER_INFO_COLUMN)); 329 numMessages = cursor.getInt(UIProvider.CONVERSATION_NUM_MESSAGES_COLUMN); 330 numDrafts = cursor.getInt(UIProvider.CONVERSATION_NUM_DRAFTS_COLUMN); 331 } 332 isRemote = cursor.getInt(UIProvider.CONVERSATION_REMOTE_COLUMN) != 0; 333 } 334 } 335 336 public Conversation(Conversation other) { 337 if (other == null) { 338 return; 339 } 340 341 id = other.id; 342 uri = other.uri; 343 dateMs = other.dateMs; 344 subject = other.subject; 345 hasAttachments = other.hasAttachments; 346 messageListUri = other.messageListUri; 347 sendingState = other.sendingState; 348 priority = other.priority; 349 read = other.read; 350 seen = other.seen; 351 starred = other.starred; 352 rawFolders = other.rawFolders; // FolderList is immutable, shallow copy is OK 353 convFlags = other.convFlags; 354 personalLevel = other.personalLevel; 355 spam = other.spam; 356 phishing = other.phishing; 357 muted = other.muted; 358 color = other.color; 359 accountUri = other.accountUri; 360 position = other.position; 361 localDeleteOnUpdate = other.localDeleteOnUpdate; 362 // although ConversationInfo is mutable (see ConversationInfo.markRead), applyCachedValues 363 // will overwrite this if cached changes exist anyway, so a shallow copy is OK 364 conversationInfo = other.conversationInfo; 365 conversationBaseUri = other.conversationBaseUri; 366 snippet = other.snippet; 367 senders = other.senders; 368 numMessages = other.numMessages; 369 numDrafts = other.numDrafts; 370 isRemote = other.isRemote; 371 } 372 373 public Conversation() { 374 } 375 376 public static Conversation create(long id, Uri uri, String subject, long dateMs, 377 String snippet, boolean hasAttachment, Uri messageListUri, String senders, 378 int numMessages, int numDrafts, int sendingState, int priority, boolean read, 379 boolean seen, boolean starred, FolderList rawFolders, int convFlags, int personalLevel, 380 boolean spam, boolean phishing, boolean muted, Uri accountUri, 381 ConversationInfo conversationInfo, Uri conversationBase, boolean isRemote) { 382 383 final Conversation conversation = new Conversation(); 384 385 conversation.id = id; 386 conversation.uri = uri; 387 conversation.subject = subject; 388 conversation.dateMs = dateMs; 389 conversation.snippet = snippet; 390 conversation.hasAttachments = hasAttachment; 391 conversation.messageListUri = messageListUri; 392 conversation.senders = emptyIfNull(senders); 393 conversation.numMessages = numMessages; 394 conversation.numDrafts = numDrafts; 395 conversation.sendingState = sendingState; 396 conversation.priority = priority; 397 conversation.read = read; 398 conversation.seen = seen; 399 conversation.starred = starred; 400 conversation.rawFolders = rawFolders; 401 conversation.convFlags = convFlags; 402 conversation.personalLevel = personalLevel; 403 conversation.spam = spam; 404 conversation.phishing = phishing; 405 conversation.muted = muted; 406 conversation.color = 0; 407 conversation.accountUri = accountUri; 408 conversation.conversationInfo = conversationInfo; 409 conversation.conversationBaseUri = conversationBase; 410 conversation.isRemote = isRemote; 411 return conversation; 412 } 413 414 /** 415 * Apply any column values from the given {@link ContentValues} (where column names are the 416 * keys) to this conversation. 417 * 418 */ 419 public void applyCachedValues(ContentValues values) { 420 if (values == null) { 421 return; 422 } 423 for (String key : values.keySet()) { 424 final Object val = values.get(key); 425 LogUtils.i(LOG_TAG, "Conversation: applying cached value to col=%s val=%s", key, 426 val); 427 if (ConversationColumns.READ.equals(key)) { 428 read = (Integer) val != 0; 429 } else if (ConversationColumns.CONVERSATION_INFO.equals(key)) { 430 conversationInfo = ConversationInfo.fromBlob((byte[]) val); 431 } else if (ConversationColumns.FLAGS.equals(key)) { 432 convFlags = (Integer) val; 433 } else if (ConversationColumns.STARRED.equals(key)) { 434 starred = (Integer) val != 0; 435 } else if (ConversationColumns.SEEN.equals(key)) { 436 seen = (Integer) val != 0; 437 } else if (ConversationColumns.RAW_FOLDERS.equals(key)) { 438 rawFolders = FolderList.fromBlob((byte[]) val); 439 } else if (ConversationColumns.VIEWED.equals(key)) { 440 // ignore. this is not read from the cursor, either. 441 } else { 442 LogUtils.e(LOG_TAG, new UnsupportedOperationException(), 443 "unsupported cached conv value in col=%s", key); 444 } 445 } 446 } 447 448 /** 449 * Get the <strong>immutable</strong> list of {@link Folder}s for this conversation. To modify 450 * this list, make a new {@link FolderList} and use {@link #setRawFolders(FolderList)}. 451 * 452 * @return <strong>Immutable</strong> list of {@link Folder}s. 453 */ 454 public List<Folder> getRawFolders() { 455 return rawFolders.folders; 456 } 457 458 public void setRawFolders(FolderList folders) { 459 clearCachedFolders(); 460 rawFolders = folders; 461 } 462 463 private void clearCachedFolders() { 464 cachedDisplayableFolders = null; 465 } 466 467 public ArrayList<Folder> getRawFoldersForDisplay(final Uri ignoreFolderUri, 468 final int ignoreFolderType) { 469 if (cachedDisplayableFolders == null) { 470 cachedDisplayableFolders = new ArrayList<Folder>(); 471 for (Folder folder : rawFolders.folders) { 472 // skip the ignoreFolder 473 if (ignoreFolderUri != null && ignoreFolderUri.equals(folder.uri)) { 474 continue; 475 } 476 // Skip the ignoreFolderType 477 if (ignoreFolderType >= 0 && folder.isType(ignoreFolderType)) { 478 continue; 479 } 480 cachedDisplayableFolders.add(folder); 481 } 482 } 483 return cachedDisplayableFolders; 484 } 485 486 @Override 487 public boolean equals(Object o) { 488 if (o instanceof Conversation) { 489 Conversation conv = (Conversation) o; 490 return conv.uri.equals(uri); 491 } 492 return false; 493 } 494 495 @Override 496 public int hashCode() { 497 return uri.hashCode(); 498 } 499 500 /** 501 * Get if this conversation is marked as high priority. 502 */ 503 public boolean isImportant() { 504 return priority == UIProvider.ConversationPriority.IMPORTANT; 505 } 506 507 /** 508 * Get if this conversation is mostly dead 509 */ 510 public boolean isMostlyDead() { 511 return (convFlags & FLAG_MOSTLY_DEAD) != 0; 512 } 513 514 /** 515 * Returns true if the URI of the conversation specified as the needle was 516 * found in the collection of conversations specified as the haystack. False 517 * otherwise. This method is safe to call with null arguments. 518 * 519 * @param haystack 520 * @param needle 521 * @return true if the needle was found in the haystack, false otherwise. 522 */ 523 public final static boolean contains(Collection<Conversation> haystack, Conversation needle) { 524 // If the haystack is empty, it cannot contain anything. 525 if (haystack == null || haystack.size() <= 0) { 526 return false; 527 } 528 // The null folder exists everywhere. 529 if (needle == null) { 530 return true; 531 } 532 final long toFind = needle.id; 533 for (final Conversation c : haystack) { 534 if (toFind == c.id) { 535 return true; 536 } 537 } 538 return false; 539 } 540 541 /** 542 * Returns a collection of a single conversation. This method always returns 543 * a valid collection even if the input conversation is null. 544 * 545 * @param in a conversation, possibly null. 546 * @return a collection of the conversation. 547 */ 548 public static Collection<Conversation> listOf(Conversation in) { 549 final Collection<Conversation> target = (in == null) ? EMPTY : ImmutableList.of(in); 550 return target; 551 } 552 553 /** 554 * Get the snippet for this conversation. Masks that it may come from 555 * conversation info or the original deprecated snippet string. 556 */ 557 public String getSnippet() { 558 return conversationInfo != null && !TextUtils.isEmpty(conversationInfo.firstSnippet) ? 559 conversationInfo.firstSnippet : snippet; 560 } 561 562 /** 563 * Get the number of messages for this conversation. 564 */ 565 public int getNumMessages() { 566 return conversationInfo != null ? conversationInfo.messageCount : numMessages; 567 } 568 569 /** 570 * Get the number of drafts for this conversation. 571 */ 572 public int numDrafts() { 573 return conversationInfo != null ? conversationInfo.draftCount : numDrafts; 574 } 575 576 public boolean isViewed() { 577 return viewed; 578 } 579 580 public void markViewed() { 581 viewed = true; 582 } 583 584 public String getBaseUri(String defaultValue) { 585 return conversationBaseUri != null ? conversationBaseUri.toString() : defaultValue; 586 } 587 588 public int getAttachmentsCount() { 589 return getAttachments().size(); 590 } 591 592 public ArrayList<String> getAttachments() { 593 return Lists.newArrayList(); 594 } 595 596 /** 597 * Create a human-readable string of all the conversations 598 * @param collection Any collection of conversations 599 * @return string with a human readable representation of the conversations. 600 */ 601 public static String toString(Collection<Conversation> collection) { 602 final StringBuilder out = new StringBuilder(collection.size() + " conversations:"); 603 int count = 0; 604 for (final Conversation c : collection) { 605 count++; 606 // Indent the conversations to make them easy to read in debug 607 // output. 608 out.append(" " + count + ": " + c.toString() + "\n"); 609 } 610 return out.toString(); 611 } 612 613 /** 614 * Returns an empty string if the specified string is null 615 */ 616 private static String emptyIfNull(String in) { 617 return in != null ? in : EMPTY_STRING; 618 } 619 620 /** 621 * Get the properly formatted subject and snippet string for display a 622 * conversation. 623 * 624 * @param context 625 * @param filteredSubject 626 * @param snippet 627 */ 628 public static String getSubjectAndSnippetForDisplay(Context context, 629 String filteredSubject, String snippet) { 630 if (sSubjectAndSnippet == null) { 631 sSubjectAndSnippet = context.getString(R.string.subject_and_snippet); 632 } 633 if (TextUtils.isEmpty(filteredSubject) && TextUtils.isEmpty(snippet)) { 634 return ""; 635 } else if (TextUtils.isEmpty(filteredSubject)) { 636 return snippet; 637 } else if (TextUtils.isEmpty(snippet)) { 638 return filteredSubject; 639 } 640 641 return String.format(sSubjectAndSnippet, filteredSubject, snippet); 642 } 643} 644