Conversation.java revision 351ad4e87e0b0b98df9ca88266a8a63541dc5a81
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; 35 36public class Conversation implements Parcelable { 37 public static final int NO_POSITION = -1; 38 39 private static final String EMPTY_STRING = ""; 40 41 /** 42 * @see BaseColumns#_ID 43 */ 44 public long id; 45 /** 46 * @see UIProvider.ConversationColumns#URI 47 */ 48 public Uri uri; 49 /** 50 * @see UIProvider.ConversationColumns#SUBJECT 51 */ 52 public String subject; 53 /** 54 * @see UIProvider.ConversationColumns#DATE_RECEIVED_MS 55 */ 56 public long dateMs; 57 /** 58 * @see UIProvider.ConversationColumns#SNIPPET 59 */ 60 @Deprecated 61 public String snippet; 62 /** 63 * @see UIProvider.ConversationColumns#HAS_ATTACHMENTS 64 */ 65 public boolean hasAttachments; 66 /** 67 * @see UIProvider.ConversationColumns#MESSAGE_LIST_URI 68 */ 69 public Uri messageListUri; 70 /** 71 * @see UIProvider.ConversationColumns#SENDER_INFO 72 */ 73 @Deprecated 74 public String senders; 75 /** 76 * @see UIProvider.ConversationColumns#NUM_MESSAGES 77 */ 78 private int numMessages; 79 /** 80 * @see UIProvider.ConversationColumns#NUM_DRAFTS 81 */ 82 private int numDrafts; 83 /** 84 * @see UIProvider.ConversationColumns#SENDING_STATE 85 */ 86 public int sendingState; 87 /** 88 * @see UIProvider.ConversationColumns#PRIORITY 89 */ 90 public int priority; 91 /** 92 * @see UIProvider.ConversationColumns#READ 93 */ 94 public boolean read; 95 /** 96 * @see UIProvider.ConversationColumns#STARRED 97 */ 98 public boolean starred; 99 /** 100 * @see UIProvider.ConversationColumns#RAW_FOLDERS 101 */ 102 private String rawFolders; 103 /** 104 * @see UIProvider.ConversationColumns#FLAGS 105 */ 106 public int convFlags; 107 /** 108 * @see UIProvider.ConversationColumns#PERSONAL_LEVEL 109 */ 110 public int personalLevel; 111 /** 112 * @see UIProvider.ConversationColumns#SPAM 113 */ 114 public boolean spam; 115 /** 116 * @see UIProvider.ConversationColumns#MUTED 117 */ 118 public boolean muted; 119 /** 120 * @see UIProvider.ConversationColumns#PHISHING 121 */ 122 public boolean phishing; 123 /** 124 * @see UIProvider.ConversationColumns#COLOR 125 */ 126 public int color; 127 /** 128 * @see UIProvider.ConversationColumns#ACCOUNT_URI 129 */ 130 public Uri accountUri; 131 /** 132 * @see UIProvider.ConversationColumns#CONVERSATION_INFO 133 */ 134 public ConversationInfo conversationInfo; 135 /** 136 * @see UIProvider.ConversationColumns#CONVERSATION_BASE_URI 137 */ 138 public Uri conversationBaseUri; 139 /** 140 * @see UIProvider.ConversationColumns#REMOTE 141 */ 142 public boolean isRemote; 143 144 // Used within the UI to indicate the adapter position of this conversation 145 public transient int position; 146 // Used within the UI to indicate that a Conversation should be removed from 147 // the ConversationCursor when executing an update, e.g. the the 148 // Conversation is no longer in the ConversationList for the current folder, 149 // that is it's now in some other folder(s) 150 public transient boolean localDeleteOnUpdate; 151 152 private transient boolean viewed; 153 154 private ArrayList<Folder> cachedRawFolders; 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.writeString(rawFolders); 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.readString(); 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 = cursor.getString(UIProvider.CONVERSATION_RAW_FOLDERS_COLUMN); 284 convFlags = cursor.getInt(UIProvider.CONVERSATION_FLAGS_COLUMN); 285 personalLevel = cursor.getInt(UIProvider.CONVERSATION_PERSONAL_LEVEL_COLUMN); 286 spam = cursor.getInt(UIProvider.CONVERSATION_IS_SPAM_COLUMN) != 0; 287 phishing = cursor.getInt(UIProvider.CONVERSATION_IS_PHISHING_COLUMN) != 0; 288 muted = cursor.getInt(UIProvider.CONVERSATION_MUTED_COLUMN) != 0; 289 color = cursor.getInt(UIProvider.CONVERSATION_COLOR_COLUMN); 290 String account = cursor.getString(UIProvider.CONVERSATION_ACCOUNT_URI_COLUMN); 291 accountUri = !TextUtils.isEmpty(account) ? Uri.parse(account) : null; 292 position = NO_POSITION; 293 localDeleteOnUpdate = false; 294 conversationInfo = ConversationInfo.fromBlob( 295 cursor.getBlob(UIProvider.CONVERSATION_INFO_COLUMN)); 296 final String conversationBase = 297 cursor.getString(UIProvider.CONVERSATION_BASE_URI_COLUMN); 298 conversationBaseUri = !TextUtils.isEmpty(conversationBase) ? 299 Uri.parse(conversationBase) : null; 300 if (conversationInfo == null) { 301 snippet = cursor.getString(UIProvider.CONVERSATION_SNIPPET_COLUMN); 302 senders = emptyIfNull(cursor.getString(UIProvider.CONVERSATION_SENDER_INFO_COLUMN)); 303 numMessages = cursor.getInt(UIProvider.CONVERSATION_NUM_MESSAGES_COLUMN); 304 numDrafts = cursor.getInt(UIProvider.CONVERSATION_NUM_DRAFTS_COLUMN); 305 } 306 isRemote = cursor.getInt(UIProvider.CONVERSATION_REMOTE_COLUMN) != 0; 307 } 308 } 309 310 public Conversation() { 311 } 312 313 public static Conversation create(long id, Uri uri, String subject, long dateMs, 314 String snippet, boolean hasAttachment, Uri messageListUri, String senders, 315 int numMessages, int numDrafts, int sendingState, int priority, boolean read, 316 boolean starred, String rawFolders, int convFlags, int personalLevel, boolean spam, 317 boolean phishing, boolean muted, Uri accountUri, ConversationInfo conversationInfo, 318 Uri conversationBase, boolean isRemote) { 319 320 final Conversation conversation = new Conversation(); 321 322 conversation.id = id; 323 conversation.uri = uri; 324 conversation.subject = subject; 325 conversation.dateMs = dateMs; 326 conversation.snippet = snippet; 327 conversation.hasAttachments = hasAttachment; 328 conversation.messageListUri = messageListUri; 329 conversation.senders = emptyIfNull(senders); 330 conversation.numMessages = numMessages; 331 conversation.numDrafts = numDrafts; 332 conversation.sendingState = sendingState; 333 conversation.priority = priority; 334 conversation.read = read; 335 conversation.starred = starred; 336 conversation.rawFolders = rawFolders; 337 conversation.convFlags = convFlags; 338 conversation.personalLevel = personalLevel; 339 conversation.spam = spam; 340 conversation.phishing = phishing; 341 conversation.muted = muted; 342 conversation.color = 0; 343 conversation.accountUri = accountUri; 344 conversation.conversationInfo = conversationInfo; 345 conversation.conversationBaseUri = conversationBase; 346 conversation.isRemote = isRemote; 347 return conversation; 348 } 349 350 public ArrayList<Folder> getRawFolders() { 351 if (cachedRawFolders == null) { 352 // Create cached folders. 353 if (!TextUtils.isEmpty(rawFolders)) { 354 cachedRawFolders = Folder.getFoldersArray(rawFolders); 355 } else { 356 return new ArrayList<Folder>(); 357 } 358 } 359 return cachedRawFolders; 360 } 361 362 public void setRawFolders(String raw) { 363 clearCachedFolders(); 364 rawFolders = raw; 365 } 366 367 public String getRawFoldersString() { 368 return rawFolders; 369 } 370 371 private void clearCachedFolders() { 372 cachedRawFolders = null; 373 cachedDisplayableFolders = null; 374 } 375 376 public ArrayList<Folder> getRawFoldersForDisplay(Folder ignoreFolder) { 377 ArrayList<Folder> folders = getRawFolders(); 378 if (cachedDisplayableFolders == null) { 379 cachedDisplayableFolders = new ArrayList<Folder>(); 380 for (Folder folder : folders) { 381 // skip the ignoreFolder 382 if (ignoreFolder != null && ignoreFolder.equals(folder)) { 383 continue; 384 } 385 cachedDisplayableFolders.add(folder); 386 } 387 } 388 return cachedDisplayableFolders; 389 } 390 391 @Override 392 public boolean equals(Object o) { 393 if (o instanceof Conversation) { 394 Conversation conv = (Conversation) o; 395 return conv.uri.equals(uri); 396 } 397 return false; 398 } 399 400 @Override 401 public int hashCode() { 402 return uri.hashCode(); 403 } 404 405 /** 406 * Get if this conversation is marked as high priority. 407 */ 408 public boolean isImportant() { 409 return priority == UIProvider.ConversationPriority.IMPORTANT; 410 } 411 412 /** 413 * Get if this conversation is mostly dead 414 */ 415 public boolean isMostlyDead() { 416 return (convFlags & FLAG_MOSTLY_DEAD) != 0; 417 } 418 419 /** 420 * Returns true if the URI of the conversation specified as the needle was 421 * found in the collection of conversations specified as the haystack. False 422 * otherwise. This method is safe to call with null arguments. 423 * 424 * @param haystack 425 * @param needle 426 * @return true if the needle was found in the haystack, false otherwise. 427 */ 428 public final static boolean contains(Collection<Conversation> haystack, Conversation needle) { 429 // If the haystack is empty, it cannot contain anything. 430 if (haystack == null || haystack.size() <= 0) { 431 return false; 432 } 433 // The null folder exists everywhere. 434 if (needle == null) { 435 return true; 436 } 437 final long toFind = needle.id; 438 for (final Conversation c : haystack) { 439 if (toFind == c.id) { 440 return true; 441 } 442 } 443 return false; 444 } 445 446 /** 447 * Returns a collection of a single conversation. This method always returns 448 * a valid collection even if the input conversation is null. 449 * 450 * @param in a conversation, possibly null. 451 * @return a collection of the conversation. 452 */ 453 public static Collection<Conversation> listOf(Conversation in) { 454 final Collection<Conversation> target = (in == null) ? EMPTY : ImmutableList.of(in); 455 return target; 456 } 457 458 /** 459 * Get the snippet for this conversation. Masks that it may come from 460 * conversation info or the original deprecated snippet string. 461 */ 462 public String getSnippet() { 463 return conversationInfo != null && !TextUtils.isEmpty(conversationInfo.firstSnippet) ? 464 conversationInfo.firstSnippet : snippet; 465 } 466 467 public String getSenders(Context context) { 468 if (conversationInfo != null) { 469 ArrayList<String> senders = new ArrayList<String>(); 470 for (MessageInfo m : this.conversationInfo.messageInfos) { 471 senders.add(m.sender); 472 } 473 return TextUtils.join(getSendersDelimeter(context), senders); 474 } else { 475 return senders; 476 } 477 } 478 479 private String getSendersDelimeter(Context context) { 480 if (sSendersDelimeter == null) { 481 sSendersDelimeter = context.getResources().getString(R.string.senders_split_token); 482 } 483 return sSendersDelimeter; 484 } 485 486 /** 487 * Get the number of messages for this conversation. 488 */ 489 public int getNumMessages() { 490 return conversationInfo != null ? conversationInfo.messageCount : numMessages; 491 } 492 493 /** 494 * Get the number of drafts for this conversation. 495 */ 496 public int numDrafts() { 497 return conversationInfo != null ? conversationInfo.draftCount : numDrafts; 498 } 499 500 public boolean isViewed() { 501 return viewed; 502 } 503 504 public void markViewed() { 505 viewed = true; 506 } 507 508 /** 509 * Create a human-readable string of all the conversations 510 * @param collection Any collection of conversations 511 * @return string with a human readable representation of the conversations. 512 */ 513 public static String toString(Collection<Conversation> collection) { 514 final StringBuilder out = new StringBuilder(collection.size() + " conversations:"); 515 int count = 0; 516 for (final Conversation c : collection) { 517 count++; 518 // Indent the conversations to make them easy to read in debug 519 // output. 520 out.append(" " + count + ": " + c.toString() + "\n"); 521 } 522 return out.toString(); 523 } 524 525 /** 526 * Returns an empty string if the specified string is null 527 */ 528 private static String emptyIfNull(String in) { 529 return in != null ? in : EMPTY_STRING; 530 } 531 532 /** 533 * Get the properly formatted subject and snippet string for display a conversation. 534 */ 535 public static SpannableStringBuilder getSubjectAndSnippetForDisplay(Context context, 536 String filteredSubject, String snippet) { 537 if (sSubjectAndSnippet == null) { 538 sSubjectAndSnippet = context.getString(R.string.subject_and_snippet); 539 } 540 return new SpannableStringBuilder((!TextUtils.isEmpty(snippet)) ? 541 String.format(sSubjectAndSnippet, filteredSubject, snippet) 542 : filteredSubject); 543 } 544} 545