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