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