Conversation.java revision 817d6658562f7a2bbbc8255b2e10ea3ff1864e20
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.Context; 20import android.database.Cursor; 21import android.net.Uri; 22import android.os.Parcel; 23import android.os.Parcelable; 24import android.provider.BaseColumns; 25import android.text.TextUtils; 26 27import com.android.mail.R; 28import com.android.mail.providers.UIProvider.ConversationColumns; 29import com.android.mail.utils.LogTag; 30import com.android.mail.utils.LogUtils; 31import com.google.common.collect.ImmutableList; 32 33import java.util.ArrayList; 34import java.util.Collection; 35import java.util.Collections; 36import java.util.List; 37 38public class Conversation implements Parcelable { 39 public static final int NO_POSITION = -1; 40 41 private static final String LOG_TAG = LogTag.getLogTag(); 42 43 private static final String EMPTY_STRING = ""; 44 45 /** 46 * @see BaseColumns#_ID 47 */ 48 public long id; 49 /** 50 * @see UIProvider.ConversationColumns#URI 51 */ 52 public Uri uri; 53 /** 54 * @see UIProvider.ConversationColumns#SUBJECT 55 */ 56 public String subject; 57 /** 58 * @see UIProvider.ConversationColumns#DATE_RECEIVED_MS 59 */ 60 public long dateMs; 61 /** 62 * @see UIProvider.ConversationColumns#SNIPPET 63 */ 64 @Deprecated 65 public String snippet; 66 /** 67 * @see UIProvider.ConversationColumns#HAS_ATTACHMENTS 68 */ 69 public boolean hasAttachments; 70 /** 71 * @see UIProvider.ConversationColumns#MESSAGE_LIST_URI 72 */ 73 public Uri messageListUri; 74 /** 75 * @see UIProvider.ConversationColumns#SENDER_INFO 76 */ 77 @Deprecated 78 public String senders; 79 /** 80 * @see UIProvider.ConversationColumns#NUM_MESSAGES 81 */ 82 private int numMessages; 83 /** 84 * @see UIProvider.ConversationColumns#NUM_DRAFTS 85 */ 86 private int numDrafts; 87 /** 88 * @see UIProvider.ConversationColumns#SENDING_STATE 89 */ 90 public int sendingState; 91 /** 92 * @see UIProvider.ConversationColumns#PRIORITY 93 */ 94 public int priority; 95 /** 96 * @see UIProvider.ConversationColumns#READ 97 */ 98 public boolean read; 99 /** 100 * @see UIProvider.ConversationColumns#SEEN 101 */ 102 public boolean seen; 103 /** 104 * @see UIProvider.ConversationColumns#STARRED 105 */ 106 public boolean starred; 107 /** 108 * @see UIProvider.ConversationColumns#RAW_FOLDERS 109 */ 110 private FolderList rawFolders; 111 /** 112 * @see UIProvider.ConversationColumns#FLAGS 113 */ 114 public int convFlags; 115 /** 116 * @see UIProvider.ConversationColumns#PERSONAL_LEVEL 117 */ 118 public int personalLevel; 119 /** 120 * @see UIProvider.ConversationColumns#SPAM 121 */ 122 public boolean spam; 123 /** 124 * @see UIProvider.ConversationColumns#MUTED 125 */ 126 public boolean muted; 127 /** 128 * @see UIProvider.ConversationColumns#PHISHING 129 */ 130 public boolean phishing; 131 /** 132 * @see UIProvider.ConversationColumns#COLOR 133 */ 134 public int color; 135 /** 136 * @see UIProvider.ConversationColumns#ACCOUNT_URI 137 */ 138 public Uri accountUri; 139 /** 140 * @see UIProvider.ConversationColumns#CONVERSATION_INFO 141 */ 142 public ConversationInfo conversationInfo; 143 /** 144 * @see UIProvider.ConversationColumns#CONVERSATION_BASE_URI 145 */ 146 public Uri conversationBaseUri; 147 /** 148 * @see UIProvider.ConversationColumns#REMOTE 149 */ 150 public boolean isRemote; 151 152 // Used within the UI to indicate the adapter position of this conversation 153 public transient int position; 154 // Used within the UI to indicate that a Conversation should be removed from 155 // the ConversationCursor when executing an update, e.g. the the 156 // Conversation is no longer in the ConversationList for the current folder, 157 // that is it's now in some other folder(s) 158 public transient boolean localDeleteOnUpdate; 159 160 private transient boolean viewed; 161 162 private ArrayList<Folder> cachedDisplayableFolders; 163 164 private static String sSendersDelimeter; 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() { 330 } 331 332 public static Conversation create(long id, Uri uri, String subject, long dateMs, 333 String snippet, boolean hasAttachment, Uri messageListUri, String senders, 334 int numMessages, int numDrafts, int sendingState, int priority, boolean read, 335 boolean seen, boolean starred, FolderList rawFolders, int convFlags, int personalLevel, 336 boolean spam, boolean phishing, boolean muted, Uri accountUri, 337 ConversationInfo conversationInfo, Uri conversationBase, boolean isRemote) { 338 339 final Conversation conversation = new Conversation(); 340 341 conversation.id = id; 342 conversation.uri = uri; 343 conversation.subject = subject; 344 conversation.dateMs = dateMs; 345 conversation.snippet = snippet; 346 conversation.hasAttachments = hasAttachment; 347 conversation.messageListUri = messageListUri; 348 conversation.senders = emptyIfNull(senders); 349 conversation.numMessages = numMessages; 350 conversation.numDrafts = numDrafts; 351 conversation.sendingState = sendingState; 352 conversation.priority = priority; 353 conversation.read = read; 354 conversation.seen = seen; 355 conversation.starred = starred; 356 conversation.rawFolders = rawFolders; 357 conversation.convFlags = convFlags; 358 conversation.personalLevel = personalLevel; 359 conversation.spam = spam; 360 conversation.phishing = phishing; 361 conversation.muted = muted; 362 conversation.color = 0; 363 conversation.accountUri = accountUri; 364 conversation.conversationInfo = conversationInfo; 365 conversation.conversationBaseUri = conversationBase; 366 conversation.isRemote = isRemote; 367 return conversation; 368 } 369 370 /** 371 * Get the <strong>immutable</strong> list of {@link Folder}s for this conversation. To modify 372 * this list, make a new {@link FolderList} and use {@link #setRawFolders(FolderList)}. 373 * 374 * @return <strong>Immutable</strong> list of {@link Folder}s. 375 */ 376 public List<Folder> getRawFolders() { 377 return rawFolders.folders; 378 } 379 380 public void setRawFolders(FolderList folders) { 381 clearCachedFolders(); 382 rawFolders = folders; 383 } 384 385 private void clearCachedFolders() { 386 cachedDisplayableFolders = null; 387 } 388 389 public ArrayList<Folder> getRawFoldersForDisplay(Folder ignoreFolder) { 390 if (cachedDisplayableFolders == null) { 391 cachedDisplayableFolders = new ArrayList<Folder>(); 392 for (Folder folder : rawFolders.folders) { 393 // skip the ignoreFolder 394 if (ignoreFolder != null && ignoreFolder.equals(folder)) { 395 continue; 396 } 397 cachedDisplayableFolders.add(folder); 398 } 399 } 400 return cachedDisplayableFolders; 401 } 402 403 @Override 404 public boolean equals(Object o) { 405 if (o instanceof Conversation) { 406 Conversation conv = (Conversation) o; 407 return conv.uri.equals(uri); 408 } 409 return false; 410 } 411 412 @Override 413 public int hashCode() { 414 return uri.hashCode(); 415 } 416 417 /** 418 * Get if this conversation is marked as high priority. 419 */ 420 public boolean isImportant() { 421 return priority == UIProvider.ConversationPriority.IMPORTANT; 422 } 423 424 /** 425 * Get if this conversation is mostly dead 426 */ 427 public boolean isMostlyDead() { 428 return (convFlags & FLAG_MOSTLY_DEAD) != 0; 429 } 430 431 /** 432 * Returns true if the URI of the conversation specified as the needle was 433 * found in the collection of conversations specified as the haystack. False 434 * otherwise. This method is safe to call with null arguments. 435 * 436 * @param haystack 437 * @param needle 438 * @return true if the needle was found in the haystack, false otherwise. 439 */ 440 public final static boolean contains(Collection<Conversation> haystack, Conversation needle) { 441 // If the haystack is empty, it cannot contain anything. 442 if (haystack == null || haystack.size() <= 0) { 443 return false; 444 } 445 // The null folder exists everywhere. 446 if (needle == null) { 447 return true; 448 } 449 final long toFind = needle.id; 450 for (final Conversation c : haystack) { 451 if (toFind == c.id) { 452 return true; 453 } 454 } 455 return false; 456 } 457 458 /** 459 * Returns a collection of a single conversation. This method always returns 460 * a valid collection even if the input conversation is null. 461 * 462 * @param in a conversation, possibly null. 463 * @return a collection of the conversation. 464 */ 465 public static Collection<Conversation> listOf(Conversation in) { 466 final Collection<Conversation> target = (in == null) ? EMPTY : ImmutableList.of(in); 467 return target; 468 } 469 470 /** 471 * Get the snippet for this conversation. Masks that it may come from 472 * conversation info or the original deprecated snippet string. 473 */ 474 public String getSnippet() { 475 return conversationInfo != null && !TextUtils.isEmpty(conversationInfo.firstSnippet) ? 476 conversationInfo.firstSnippet : snippet; 477 } 478 479 public String getSenders(Context context) { 480 if (conversationInfo != null) { 481 ArrayList<String> senders = new ArrayList<String>(); 482 for (MessageInfo m : this.conversationInfo.messageInfos) { 483 senders.add(m.sender); 484 } 485 return TextUtils.join(getSendersDelimeter(context), senders); 486 } else { 487 return senders; 488 } 489 } 490 491 private String getSendersDelimeter(Context context) { 492 if (sSendersDelimeter == null) { 493 sSendersDelimeter = context.getResources().getString(R.string.senders_split_token); 494 } 495 return sSendersDelimeter; 496 } 497 498 /** 499 * Get the number of messages for this conversation. 500 */ 501 public int getNumMessages() { 502 return conversationInfo != null ? conversationInfo.messageCount : numMessages; 503 } 504 505 /** 506 * Get the number of drafts for this conversation. 507 */ 508 public int numDrafts() { 509 return conversationInfo != null ? conversationInfo.draftCount : numDrafts; 510 } 511 512 public boolean isViewed() { 513 return viewed; 514 } 515 516 public void markViewed() { 517 viewed = true; 518 } 519 520 public String getBaseUri(String defaultValue) { 521 return conversationBaseUri != null ? conversationBaseUri.toString() : defaultValue; 522 } 523 524 /** 525 * Create a human-readable string of all the conversations 526 * @param collection Any collection of conversations 527 * @return string with a human readable representation of the conversations. 528 */ 529 public static String toString(Collection<Conversation> collection) { 530 final StringBuilder out = new StringBuilder(collection.size() + " conversations:"); 531 int count = 0; 532 for (final Conversation c : collection) { 533 count++; 534 // Indent the conversations to make them easy to read in debug 535 // output. 536 out.append(" " + count + ": " + c.toString() + "\n"); 537 } 538 return out.toString(); 539 } 540 541 /** 542 * Returns an empty string if the specified string is null 543 */ 544 private static String emptyIfNull(String in) { 545 return in != null ? in : EMPTY_STRING; 546 } 547 548 /** 549 * Get the properly formatted subject and snippet string for display a 550 * conversation. 551 * 552 * @param context 553 * @param filteredSubject 554 * @param snippet 555 */ 556 public static String getSubjectAndSnippetForDisplay(Context context, 557 String filteredSubject, String snippet) { 558 if (sSubjectAndSnippet == null) { 559 sSubjectAndSnippet = context.getString(R.string.subject_and_snippet); 560 } 561 return (!TextUtils.isEmpty(snippet)) ? 562 String.format(sSubjectAndSnippet, filteredSubject, snippet) 563 : filteredSubject; 564 } 565} 566