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