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