Conversation.java revision 8ebc2ce34ed7d979662cf88b1c9fa60d63142fe4
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.SEEN.equals(key)) { 430 seen = (Integer) val != 0; 431 } else if (ConversationColumns.RAW_FOLDERS.equals(key)) { 432 rawFolders = FolderList.fromBlob((byte[]) val); 433 } else if (ConversationColumns.VIEWED.equals(key)) { 434 // ignore. this is not read from the cursor, either. 435 } else { 436 LogUtils.e(LOG_TAG, new UnsupportedOperationException(), 437 "unsupported cached conv value in col=%s", key); 438 } 439 } 440 } 441 442 /** 443 * Get the <strong>immutable</strong> list of {@link Folder}s for this conversation. To modify 444 * this list, make a new {@link FolderList} and use {@link #setRawFolders(FolderList)}. 445 * 446 * @return <strong>Immutable</strong> list of {@link Folder}s. 447 */ 448 public List<Folder> getRawFolders() { 449 return rawFolders.folders; 450 } 451 452 public void setRawFolders(FolderList folders) { 453 clearCachedFolders(); 454 rawFolders = folders; 455 } 456 457 private void clearCachedFolders() { 458 cachedDisplayableFolders = null; 459 } 460 461 public ArrayList<Folder> getRawFoldersForDisplay(Folder ignoreFolder) { 462 if (cachedDisplayableFolders == null) { 463 cachedDisplayableFolders = new ArrayList<Folder>(); 464 for (Folder folder : rawFolders.folders) { 465 // skip the ignoreFolder 466 if (ignoreFolder != null && ignoreFolder.equals(folder)) { 467 continue; 468 } 469 cachedDisplayableFolders.add(folder); 470 } 471 } 472 return cachedDisplayableFolders; 473 } 474 475 @Override 476 public boolean equals(Object o) { 477 if (o instanceof Conversation) { 478 Conversation conv = (Conversation) o; 479 return conv.uri.equals(uri); 480 } 481 return false; 482 } 483 484 @Override 485 public int hashCode() { 486 return uri.hashCode(); 487 } 488 489 /** 490 * Get if this conversation is marked as high priority. 491 */ 492 public boolean isImportant() { 493 return priority == UIProvider.ConversationPriority.IMPORTANT; 494 } 495 496 /** 497 * Get if this conversation is mostly dead 498 */ 499 public boolean isMostlyDead() { 500 return (convFlags & FLAG_MOSTLY_DEAD) != 0; 501 } 502 503 /** 504 * Returns true if the URI of the conversation specified as the needle was 505 * found in the collection of conversations specified as the haystack. False 506 * otherwise. This method is safe to call with null arguments. 507 * 508 * @param haystack 509 * @param needle 510 * @return true if the needle was found in the haystack, false otherwise. 511 */ 512 public final static boolean contains(Collection<Conversation> haystack, Conversation needle) { 513 // If the haystack is empty, it cannot contain anything. 514 if (haystack == null || haystack.size() <= 0) { 515 return false; 516 } 517 // The null folder exists everywhere. 518 if (needle == null) { 519 return true; 520 } 521 final long toFind = needle.id; 522 for (final Conversation c : haystack) { 523 if (toFind == c.id) { 524 return true; 525 } 526 } 527 return false; 528 } 529 530 /** 531 * Returns a collection of a single conversation. This method always returns 532 * a valid collection even if the input conversation is null. 533 * 534 * @param in a conversation, possibly null. 535 * @return a collection of the conversation. 536 */ 537 public static Collection<Conversation> listOf(Conversation in) { 538 final Collection<Conversation> target = (in == null) ? EMPTY : ImmutableList.of(in); 539 return target; 540 } 541 542 /** 543 * Get the snippet for this conversation. Masks that it may come from 544 * conversation info or the original deprecated snippet string. 545 */ 546 public String getSnippet() { 547 return conversationInfo != null && !TextUtils.isEmpty(conversationInfo.firstSnippet) ? 548 conversationInfo.firstSnippet : snippet; 549 } 550 551 public String getSenders(Context context) { 552 if (conversationInfo != null) { 553 ArrayList<String> senders = new ArrayList<String>(); 554 for (MessageInfo m : this.conversationInfo.messageInfos) { 555 senders.add(m.sender); 556 } 557 return TextUtils.join(getSendersDelimeter(context), senders); 558 } else { 559 return senders; 560 } 561 } 562 563 private static String getSendersDelimeter(Context context) { 564 if (sSendersDelimeter == null) { 565 sSendersDelimeter = context.getResources().getString(R.string.senders_split_token); 566 } 567 return sSendersDelimeter; 568 } 569 570 /** 571 * Get the number of messages for this conversation. 572 */ 573 public int getNumMessages() { 574 return conversationInfo != null ? conversationInfo.messageCount : numMessages; 575 } 576 577 /** 578 * Get the number of drafts for this conversation. 579 */ 580 public int numDrafts() { 581 return conversationInfo != null ? conversationInfo.draftCount : numDrafts; 582 } 583 584 public boolean isViewed() { 585 return viewed; 586 } 587 588 public void markViewed() { 589 viewed = true; 590 } 591 592 public String getBaseUri(String defaultValue) { 593 return conversationBaseUri != null ? conversationBaseUri.toString() : defaultValue; 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