Conversation.java revision b8361c9f8938b74977316319885998aae09aab77
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 // Constituents of convFlags below 160 // Flag indicating that the item has been deleted, but will continue being 161 // shown in the list Delete/Archive of a mostly-dead item will NOT propagate 162 // the delete/archive, but WILL remove the item from the cursor 163 public static final int FLAG_MOSTLY_DEAD = 1 << 0; 164 165 /** An immutable, empty conversation list */ 166 public static final Collection<Conversation> EMPTY = Collections.emptyList(); 167 168 @Override 169 public int describeContents() { 170 return 0; 171 } 172 173 @Override 174 public void writeToParcel(Parcel dest, int flags) { 175 dest.writeLong(id); 176 dest.writeParcelable(uri, flags); 177 dest.writeString(subject); 178 dest.writeLong(dateMs); 179 dest.writeString(snippet); 180 dest.writeInt(hasAttachments ? 1 : 0); 181 dest.writeParcelable(messageListUri, 0); 182 dest.writeString(senders); 183 dest.writeInt(numMessages); 184 dest.writeInt(numDrafts); 185 dest.writeInt(sendingState); 186 dest.writeInt(priority); 187 dest.writeInt(read ? 1 : 0); 188 dest.writeInt(starred ? 1 : 0); 189 dest.writeString(rawFolders); 190 dest.writeInt(convFlags); 191 dest.writeInt(personalLevel); 192 dest.writeInt(spam ? 1 : 0); 193 dest.writeInt(phishing ? 1 : 0); 194 dest.writeInt(muted ? 1 : 0); 195 dest.writeInt(color); 196 dest.writeParcelable(accountUri, 0); 197 dest.writeString(ConversationInfo.toString(conversationInfo)); 198 dest.writeParcelable(conversationBaseUri, 0); 199 dest.writeInt(isRemote ? 1 : 0); 200 } 201 202 private Conversation(Parcel in) { 203 id = in.readLong(); 204 uri = in.readParcelable(null); 205 subject = in.readString(); 206 dateMs = in.readLong(); 207 snippet = in.readString(); 208 hasAttachments = (in.readInt() != 0); 209 messageListUri = in.readParcelable(null); 210 senders = emptyIfNull(in.readString()); 211 numMessages = in.readInt(); 212 numDrafts = in.readInt(); 213 sendingState = in.readInt(); 214 priority = in.readInt(); 215 read = (in.readInt() != 0); 216 starred = (in.readInt() != 0); 217 rawFolders = in.readString(); 218 convFlags = in.readInt(); 219 personalLevel = in.readInt(); 220 spam = in.readInt() != 0; 221 phishing = in.readInt() != 0; 222 muted = in.readInt() != 0; 223 color = in.readInt(); 224 accountUri = in.readParcelable(null); 225 position = NO_POSITION; 226 localDeleteOnUpdate = false; 227 conversationInfo = ConversationInfo.fromString(in.readString()); 228 conversationBaseUri = in.readParcelable(null); 229 isRemote = in.readInt() != 0; 230 } 231 232 @Override 233 public String toString() { 234 return "[conversation id=" + id + ", subject =" + subject + "]"; 235 } 236 237 public static final Creator<Conversation> CREATOR = new Creator<Conversation>() { 238 239 @Override 240 public Conversation createFromParcel(Parcel source) { 241 return new Conversation(source); 242 } 243 244 @Override 245 public Conversation[] newArray(int size) { 246 return new Conversation[size]; 247 } 248 249 }; 250 251 public static final Uri MOVE_CONVERSATIONS_URI = Uri.parse("content://moveconversations"); 252 253 /** 254 * The column that needs to be updated to change the read state of a 255 * conversation. 256 */ 257 public static final String UPDATE_FOLDER_COLUMN = ConversationColumns.RAW_FOLDERS; 258 259 public Conversation(Cursor cursor) { 260 if (cursor != null) { 261 id = cursor.getLong(UIProvider.CONVERSATION_ID_COLUMN); 262 uri = Uri.parse(cursor.getString(UIProvider.CONVERSATION_URI_COLUMN)); 263 dateMs = cursor.getLong(UIProvider.CONVERSATION_DATE_RECEIVED_MS_COLUMN); 264 subject = cursor.getString(UIProvider.CONVERSATION_SUBJECT_COLUMN); 265 // Don't allow null subject 266 if (subject == null) { 267 subject = ""; 268 } 269 hasAttachments = cursor.getInt(UIProvider.CONVERSATION_HAS_ATTACHMENTS_COLUMN) != 0; 270 String messageList = cursor.getString(UIProvider.CONVERSATION_MESSAGE_LIST_URI_COLUMN); 271 messageListUri = !TextUtils.isEmpty(messageList) ? Uri.parse(messageList) : null; 272 sendingState = cursor.getInt(UIProvider.CONVERSATION_SENDING_STATE_COLUMN); 273 priority = cursor.getInt(UIProvider.CONVERSATION_PRIORITY_COLUMN); 274 read = cursor.getInt(UIProvider.CONVERSATION_READ_COLUMN) != 0; 275 starred = cursor.getInt(UIProvider.CONVERSATION_STARRED_COLUMN) != 0; 276 rawFolders = cursor.getString(UIProvider.CONVERSATION_RAW_FOLDERS_COLUMN); 277 convFlags = cursor.getInt(UIProvider.CONVERSATION_FLAGS_COLUMN); 278 personalLevel = cursor.getInt(UIProvider.CONVERSATION_PERSONAL_LEVEL_COLUMN); 279 spam = cursor.getInt(UIProvider.CONVERSATION_IS_SPAM_COLUMN) != 0; 280 phishing = cursor.getInt(UIProvider.CONVERSATION_IS_PHISHING_COLUMN) != 0; 281 muted = cursor.getInt(UIProvider.CONVERSATION_MUTED_COLUMN) != 0; 282 color = cursor.getInt(UIProvider.CONVERSATION_COLOR_COLUMN); 283 String account = cursor.getString(UIProvider.CONVERSATION_ACCOUNT_URI_COLUMN); 284 accountUri = !TextUtils.isEmpty(account) ? Uri.parse(account) : null; 285 position = NO_POSITION; 286 localDeleteOnUpdate = false; 287 conversationInfo = ConversationInfo.fromString(cursor 288 .getString(UIProvider.CONVERSATION_INFO_COLUMN)); 289 final String conversationBase = 290 cursor.getString(UIProvider.CONVERSATION_BASE_URI_COLUMN); 291 conversationBaseUri = !TextUtils.isEmpty(conversationBase) ? 292 Uri.parse(conversationBase) : null; 293 if (conversationInfo == null) { 294 snippet = cursor.getString(UIProvider.CONVERSATION_SNIPPET_COLUMN); 295 senders = emptyIfNull(cursor.getString(UIProvider.CONVERSATION_SENDER_INFO_COLUMN)); 296 numMessages = cursor.getInt(UIProvider.CONVERSATION_NUM_MESSAGES_COLUMN); 297 numDrafts = cursor.getInt(UIProvider.CONVERSATION_NUM_DRAFTS_COLUMN); 298 } 299 isRemote = cursor.getInt(UIProvider.CONVERSATION_REMOTE_COLUMN) != 0; 300 } 301 } 302 303 public Conversation() { 304 } 305 306 public static Conversation create(long id, Uri uri, String subject, long dateMs, 307 String snippet, boolean hasAttachment, Uri messageListUri, String senders, 308 int numMessages, int numDrafts, int sendingState, int priority, boolean read, 309 boolean starred, String rawFolders, int convFlags, int personalLevel, boolean spam, 310 boolean phishing, boolean muted, Uri accountUri, ConversationInfo conversationInfo, 311 Uri conversationBase, boolean isRemote) { 312 313 final Conversation conversation = new Conversation(); 314 315 conversation.id = id; 316 conversation.uri = uri; 317 conversation.subject = subject; 318 conversation.dateMs = dateMs; 319 conversation.snippet = snippet; 320 conversation.hasAttachments = hasAttachment; 321 conversation.messageListUri = messageListUri; 322 conversation.senders = emptyIfNull(senders); 323 conversation.numMessages = numMessages; 324 conversation.numDrafts = numDrafts; 325 conversation.sendingState = sendingState; 326 conversation.priority = priority; 327 conversation.read = read; 328 conversation.starred = starred; 329 conversation.rawFolders = rawFolders; 330 conversation.convFlags = convFlags; 331 conversation.personalLevel = personalLevel; 332 conversation.spam = spam; 333 conversation.phishing = phishing; 334 conversation.muted = muted; 335 conversation.color = 0; 336 conversation.accountUri = accountUri; 337 conversation.conversationInfo = conversationInfo; 338 conversation.conversationBaseUri = conversationBase; 339 conversation.isRemote = isRemote; 340 return conversation; 341 } 342 343 public ArrayList<Folder> getRawFolders() { 344 if (cachedRawFolders == null) { 345 // Create cached folders. 346 if (!TextUtils.isEmpty(rawFolders)) { 347 cachedRawFolders = Folder.getFoldersArray(rawFolders); 348 } else { 349 return new ArrayList<Folder>(); 350 } 351 } 352 return cachedRawFolders; 353 } 354 355 public void setRawFolders(String raw) { 356 clearCachedFolders(); 357 rawFolders = raw; 358 } 359 360 public String getRawFoldersString() { 361 return rawFolders; 362 } 363 364 private void clearCachedFolders() { 365 cachedRawFolders = null; 366 cachedDisplayableFolders = null; 367 } 368 369 public ArrayList<Folder> getRawFoldersForDisplay(Folder ignoreFolder) { 370 ArrayList<Folder> folders = getRawFolders(); 371 if (cachedDisplayableFolders == null) { 372 cachedDisplayableFolders = new ArrayList<Folder>(); 373 for (Folder folder : 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 conversation. 527 */ 528 public static SpannableStringBuilder getSubjectAndSnippetForDisplay(Context context, 529 String filteredSubject, String snippet) { 530 return new SpannableStringBuilder((!TextUtils.isEmpty(snippet)) ? 531 context.getString(R.string.subject_and_snippet, filteredSubject, snippet) 532 : filteredSubject); 533 } 534} 535