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