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