Conversation.java revision 2426db8887f7f1c0c93dbab5a10663cb22575ccd
1package com.android.mms.data; 2 3import java.util.HashSet; 4import java.util.Iterator; 5import java.util.Set; 6 7import android.content.AsyncQueryHandler; 8import android.content.ContentResolver; 9import android.content.ContentUris; 10import android.content.ContentValues; 11import android.content.Context; 12import android.database.Cursor; 13import android.net.Uri; 14import android.provider.Telephony.Mms; 15import android.provider.Telephony.MmsSms; 16import android.provider.Telephony.Sms; 17import android.provider.Telephony.Threads; 18import android.provider.Telephony.Sms.Conversations; 19import android.text.TextUtils; 20import android.util.Log; 21 22import com.android.mms.LogTag; 23import com.android.mms.R; 24import com.android.mms.transaction.MessagingNotification; 25import com.android.mms.ui.MessageUtils; 26import com.android.mms.util.DraftCache; 27 28/** 29 * An interface for finding information about conversations and/or creating new ones. 30 */ 31public class Conversation { 32 private static final String TAG = "Mms/conv"; 33 private static final boolean DEBUG = false; 34 35 private static final Uri sAllThreadsUri = 36 Threads.CONTENT_URI.buildUpon().appendQueryParameter("simple", "true").build(); 37 38 private static final String[] ALL_THREADS_PROJECTION = { 39 Threads._ID, Threads.DATE, Threads.MESSAGE_COUNT, Threads.RECIPIENT_IDS, 40 Threads.SNIPPET, Threads.SNIPPET_CHARSET, Threads.READ, Threads.ERROR, 41 Threads.HAS_ATTACHMENT 42 }; 43 44 private static final String[] UNREAD_PROJECTION = { 45 Threads._ID, 46 Threads.READ 47 }; 48 49 private static final String UNREAD_SELECTION = "(read=0 OR seen=0)"; 50 51 private static final String[] SEEN_PROJECTION = new String[] { 52 "seen" 53 }; 54 55 private static final int ID = 0; 56 private static final int DATE = 1; 57 private static final int MESSAGE_COUNT = 2; 58 private static final int RECIPIENT_IDS = 3; 59 private static final int SNIPPET = 4; 60 private static final int SNIPPET_CS = 5; 61 private static final int READ = 6; 62 private static final int ERROR = 7; 63 private static final int HAS_ATTACHMENT = 8; 64 65 66 private final Context mContext; 67 68 // The thread ID of this conversation. Can be zero in the case of a 69 // new conversation where the recipient set is changing as the user 70 // types and we have not hit the database yet to create a thread. 71 private long mThreadId; 72 73 private ContactList mRecipients; // The current set of recipients. 74 private long mDate; // The last update time. 75 private int mMessageCount; // Number of messages. 76 private String mSnippet; // Text of the most recent message. 77 private boolean mHasUnreadMessages; // True if there are unread messages. 78 private boolean mHasAttachment; // True if any message has an attachment. 79 private boolean mHasError; // True if any message is in an error state. 80 81 private static ContentValues mReadContentValues; 82 private static boolean mLoadingThreads; 83 private boolean mMarkAsReadBlocked; 84 private Object mMarkAsBlockedSyncer = new Object(); 85 86 private Conversation(Context context) { 87 mContext = context; 88 mRecipients = new ContactList(); 89 mThreadId = 0; 90 } 91 92 private Conversation(Context context, long threadId, boolean allowQuery) { 93 mContext = context; 94 if (!loadFromThreadId(threadId, allowQuery)) { 95 mRecipients = new ContactList(); 96 mThreadId = 0; 97 } 98 } 99 100 private Conversation(Context context, Cursor cursor, boolean allowQuery) { 101 mContext = context; 102 fillFromCursor(context, this, cursor, allowQuery); 103 } 104 105 /** 106 * Create a new conversation with no recipients. {@link #setRecipients} can 107 * be called as many times as you like; the conversation will not be 108 * created in the database until {@link #ensureThreadId} is called. 109 */ 110 public static Conversation createNew(Context context) { 111 return new Conversation(context); 112 } 113 114 /** 115 * Find the conversation matching the provided thread ID. 116 */ 117 public static Conversation get(Context context, long threadId, boolean allowQuery) { 118 Conversation conv = Cache.get(threadId); 119 if (conv != null) 120 return conv; 121 122 conv = new Conversation(context, threadId, allowQuery); 123 try { 124 Cache.put(conv); 125 } catch (IllegalStateException e) { 126 LogTag.error("Tried to add duplicate Conversation to Cache"); 127 } 128 return conv; 129 } 130 131 /** 132 * Find the conversation matching the provided recipient set. 133 * When called with an empty recipient list, equivalent to {@link #createNew}. 134 */ 135 public static Conversation get(Context context, ContactList recipients, boolean allowQuery) { 136 // If there are no recipients in the list, make a new conversation. 137 if (recipients.size() < 1) { 138 return createNew(context); 139 } 140 141 Conversation conv = Cache.get(recipients); 142 if (conv != null) 143 return conv; 144 145 long threadId = getOrCreateThreadId(context, recipients); 146 conv = new Conversation(context, threadId, allowQuery); 147 Log.d(TAG, "Conversation.get: created new conversation " + /*conv.toString()*/ "xxxxxxx"); 148 149 if (!conv.getRecipients().equals(recipients)) { 150 Log.e(TAG, "Conversation.get: new conv's recipients don't match input recpients " 151 + /*recipients*/ "xxxxxxx"); 152 } 153 154 try { 155 Cache.put(conv); 156 } catch (IllegalStateException e) { 157 LogTag.error("Tried to add duplicate Conversation to Cache"); 158 } 159 160 return conv; 161 } 162 163 /** 164 * Find the conversation matching in the specified Uri. Example 165 * forms: {@value content://mms-sms/conversations/3} or 166 * {@value sms:+12124797990}. 167 * When called with a null Uri, equivalent to {@link #createNew}. 168 */ 169 public static Conversation get(Context context, Uri uri, boolean allowQuery) { 170 if (uri == null) { 171 return createNew(context); 172 } 173 174 if (DEBUG) Log.v(TAG, "Conversation get URI: " + uri); 175 176 // Handle a conversation URI 177 if (uri.getPathSegments().size() >= 2) { 178 try { 179 long threadId = Long.parseLong(uri.getPathSegments().get(1)); 180 if (DEBUG) { 181 Log.v(TAG, "Conversation get threadId: " + threadId); 182 } 183 return get(context, threadId, allowQuery); 184 } catch (NumberFormatException exception) { 185 LogTag.error("Invalid URI: " + uri); 186 } 187 } 188 189 String recipient = uri.getSchemeSpecificPart(); 190 return get(context, ContactList.getByNumbers(recipient, 191 allowQuery /* don't block */, true /* replace number */), allowQuery); 192 } 193 194 /** 195 * Returns true if the recipient in the uri matches the recipient list in this 196 * conversation. 197 */ 198 public boolean sameRecipient(Uri uri) { 199 int size = mRecipients.size(); 200 if (size > 1) { 201 return false; 202 } 203 if (uri == null) { 204 return size == 0; 205 } 206 if (uri.getPathSegments().size() >= 2) { 207 return false; // it's a thread id for a conversation 208 } 209 String recipient = uri.getSchemeSpecificPart(); 210 ContactList incomingRecipient = ContactList.getByNumbers(recipient, 211 false /* don't block */, false /* don't replace number */); 212 return mRecipients.equals(incomingRecipient); 213 } 214 215 /** 216 * Returns a temporary Conversation (not representing one on disk) wrapping 217 * the contents of the provided cursor. The cursor should be the one 218 * returned to your AsyncQueryHandler passed in to {@link #startQueryForAll}. 219 * The recipient list of this conversation can be empty if the results 220 * were not in cache. 221 */ 222 public static Conversation from(Context context, Cursor cursor) { 223 // First look in the cache for the Conversation and return that one. That way, all the 224 // people that are looking at the cached copy will get updated when fillFromCursor() is 225 // called with this cursor. 226 long threadId = cursor.getLong(ID); 227 if (threadId > 0) { 228 Conversation conv = Cache.get(threadId); 229 if (conv != null) { 230 fillFromCursor(context, conv, cursor, false); // update the existing conv in-place 231 return conv; 232 } 233 } 234 Conversation conv = new Conversation(context, cursor, false); 235 try { 236 Cache.put(conv); 237 } catch (IllegalStateException e) { 238 LogTag.error("Tried to add duplicate Conversation to Cache"); 239 } 240 return conv; 241 } 242 243 private void buildReadContentValues() { 244 if (mReadContentValues == null) { 245 mReadContentValues = new ContentValues(2); 246 mReadContentValues.put("read", 1); 247 mReadContentValues.put("seen", 1); 248 } 249 } 250 251 /** 252 * Marks all messages in this conversation as read and updates 253 * relevant notifications. This method returns immediately; 254 * work is dispatched to a background thread. 255 */ 256 public void markAsRead() { 257 // If we have no Uri to mark (as in the case of a conversation that 258 // has not yet made its way to disk), there's nothing to do. 259 final Uri threadUri = getUri(); 260 261 new Thread(new Runnable() { 262 public void run() { 263 synchronized(mMarkAsBlockedSyncer) { 264 if (mMarkAsReadBlocked) { 265 try { 266 mMarkAsBlockedSyncer.wait(); 267 } catch (InterruptedException e) { 268 } 269 } 270 271 if (threadUri != null) { 272 buildReadContentValues(); 273 274 // Check the read flag first. It's much faster to do a query than 275 // to do an update. Timing this function show it's about 10x faster to 276 // do the query compared to the update, even when there's nothing to 277 // update. 278 boolean needUpdate = true; 279 280 Cursor c = mContext.getContentResolver().query(threadUri, 281 UNREAD_PROJECTION, UNREAD_SELECTION, null, null); 282 if (c != null) { 283 try { 284 needUpdate = c.getCount() > 0; 285 } finally { 286 c.close(); 287 } 288 } 289 290 if (needUpdate) { 291 LogTag.debug("markAsRead: update read/seen for thread uri: " + 292 threadUri); 293 mContext.getContentResolver().update(threadUri, mReadContentValues, 294 UNREAD_SELECTION, null); 295 } 296 297 setHasUnreadMessages(false); 298 } 299 } 300 301 // Always update notifications regardless of the read state. 302 MessagingNotification.blockingUpdateAllNotifications(mContext); 303 } 304 }).start(); 305 } 306 307 public void blockMarkAsRead(boolean block) { 308 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 309 LogTag.debug("blockMarkAsRead: " + block); 310 } 311 312 synchronized(mMarkAsBlockedSyncer) { 313 if (block != mMarkAsReadBlocked) { 314 mMarkAsReadBlocked = block; 315 if (!mMarkAsReadBlocked) { 316 mMarkAsBlockedSyncer.notifyAll(); 317 } 318 } 319 320 } 321 } 322 323 /** 324 * Returns a content:// URI referring to this conversation, 325 * or null if it does not exist on disk yet. 326 */ 327 public synchronized Uri getUri() { 328 if (mThreadId <= 0) 329 return null; 330 331 return ContentUris.withAppendedId(Threads.CONTENT_URI, mThreadId); 332 } 333 334 /** 335 * Return the Uri for all messages in the given thread ID. 336 * @deprecated 337 */ 338 public static Uri getUri(long threadId) { 339 // TODO: Callers using this should really just have a Conversation 340 // and call getUri() on it, but this guarantees no blocking. 341 return ContentUris.withAppendedId(Threads.CONTENT_URI, threadId); 342 } 343 344 /** 345 * Returns the thread ID of this conversation. Can be zero if 346 * {@link #ensureThreadId} has not been called yet. 347 */ 348 public synchronized long getThreadId() { 349 return mThreadId; 350 } 351 352 /** 353 * Guarantees that the conversation has been created in the database. 354 * This will make a blocking database call if it hasn't. 355 * 356 * @return The thread ID of this conversation in the database 357 */ 358 public synchronized long ensureThreadId() { 359 if (DEBUG) { 360 LogTag.debug("ensureThreadId before: " + mThreadId); 361 } 362 if (mThreadId <= 0) { 363 mThreadId = getOrCreateThreadId(mContext, mRecipients); 364 } 365 if (DEBUG) { 366 LogTag.debug("ensureThreadId after: " + mThreadId); 367 } 368 369 return mThreadId; 370 } 371 372 public synchronized void clearThreadId() { 373 // remove ourself from the cache 374 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 375 LogTag.debug("clearThreadId old threadId was: " + mThreadId + " now zero"); 376 } 377 Cache.remove(mThreadId); 378 379 mThreadId = 0; 380 } 381 382 /** 383 * Sets the list of recipients associated with this conversation. 384 * If called, {@link #ensureThreadId} must be called before the next 385 * operation that depends on this conversation existing in the 386 * database (e.g. storing a draft message to it). 387 */ 388 public synchronized void setRecipients(ContactList list) { 389 mRecipients = list; 390 391 // Invalidate thread ID because the recipient set has changed. 392 mThreadId = 0; 393 } 394 395 /** 396 * Returns the recipient set of this conversation. 397 */ 398 public synchronized ContactList getRecipients() { 399 return mRecipients; 400 } 401 402 /** 403 * Returns true if a draft message exists in this conversation. 404 */ 405 public synchronized boolean hasDraft() { 406 if (mThreadId <= 0) 407 return false; 408 409 return DraftCache.getInstance().hasDraft(mThreadId); 410 } 411 412 /** 413 * Sets whether or not this conversation has a draft message. 414 */ 415 public synchronized void setDraftState(boolean hasDraft) { 416 if (mThreadId <= 0) 417 return; 418 419 DraftCache.getInstance().setDraftState(mThreadId, hasDraft); 420 } 421 422 /** 423 * Returns the time of the last update to this conversation in milliseconds, 424 * on the {@link System#currentTimeMillis} timebase. 425 */ 426 public synchronized long getDate() { 427 return mDate; 428 } 429 430 /** 431 * Returns the number of messages in this conversation, excluding the draft 432 * (if it exists). 433 */ 434 public synchronized int getMessageCount() { 435 return mMessageCount; 436 } 437 438 /** 439 * Returns a snippet of text from the most recent message in the conversation. 440 */ 441 public synchronized String getSnippet() { 442 return mSnippet; 443 } 444 445 /** 446 * Returns true if there are any unread messages in the conversation. 447 */ 448 public boolean hasUnreadMessages() { 449 synchronized (this) { 450 return mHasUnreadMessages; 451 } 452 } 453 454 private void setHasUnreadMessages(boolean flag) { 455 synchronized (this) { 456 mHasUnreadMessages = flag; 457 } 458 } 459 460 /** 461 * Returns true if any messages in the conversation have attachments. 462 */ 463 public synchronized boolean hasAttachment() { 464 return mHasAttachment; 465 } 466 467 /** 468 * Returns true if any messages in the conversation are in an error state. 469 */ 470 public synchronized boolean hasError() { 471 return mHasError; 472 } 473 474 private static long getOrCreateThreadId(Context context, ContactList list) { 475 HashSet<String> recipients = new HashSet<String>(); 476 Contact cacheContact = null; 477 for (Contact c : list) { 478 cacheContact = Contact.get(c.getNumber(), false); 479 if (cacheContact != null) { 480 recipients.add(cacheContact.getNumber()); 481 } else { 482 recipients.add(c.getNumber()); 483 } 484 } 485 long retVal = Threads.getOrCreateThreadId(context, recipients); 486 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 487 LogTag.debug("[Conversation] getOrCreateThreadId for (%s) returned %d", 488 recipients, retVal); 489 } 490 491 return retVal; 492 } 493 494 /* 495 * The primary key of a conversation is its recipient set; override 496 * equals() and hashCode() to just pass through to the internal 497 * recipient sets. 498 */ 499 @Override 500 public synchronized boolean equals(Object obj) { 501 try { 502 Conversation other = (Conversation)obj; 503 return (mRecipients.equals(other.mRecipients)); 504 } catch (ClassCastException e) { 505 return false; 506 } 507 } 508 509 @Override 510 public synchronized int hashCode() { 511 return mRecipients.hashCode(); 512 } 513 514 @Override 515 public synchronized String toString() { 516 return String.format("[%s] (tid %d)", mRecipients.serialize(), mThreadId); 517 } 518 519 /** 520 * Remove any obsolete conversations sitting around on disk. Obsolete threads are threads 521 * that aren't referenced by any message in the pdu or sms tables. 522 */ 523 public static void asyncDeleteObsoleteThreads(AsyncQueryHandler handler, int token) { 524 handler.startDelete(token, null, Threads.OBSOLETE_THREADS_URI, null, null); 525 } 526 527 /** 528 * Start a query for all conversations in the database on the specified 529 * AsyncQueryHandler. 530 * 531 * @param handler An AsyncQueryHandler that will receive onQueryComplete 532 * upon completion of the query 533 * @param token The token that will be passed to onQueryComplete 534 */ 535 public static void startQueryForAll(AsyncQueryHandler handler, int token) { 536 handler.cancelOperation(token); 537 538 // This query looks like this in the log: 539 // I/Database( 147): elapsedTime4Sql|/data/data/com.android.providers.telephony/databases/ 540 // mmssms.db|2.253 ms|SELECT _id, date, message_count, recipient_ids, snippet, snippet_cs, 541 // read, error, has_attachment FROM threads ORDER BY date DESC 542 543 startQuery(handler, token, null); 544 } 545 546 /** 547 * Start a query for in the database on the specified AsyncQueryHandler with the specified 548 * "where" clause. 549 * 550 * @param handler An AsyncQueryHandler that will receive onQueryComplete 551 * upon completion of the query 552 * @param token The token that will be passed to onQueryComplete 553 * @param selection A where clause (can be null) to select particular conv items. 554 */ 555 public static void startQuery(AsyncQueryHandler handler, int token, String selection) { 556 handler.cancelOperation(token); 557 558 // This query looks like this in the log: 559 // I/Database( 147): elapsedTime4Sql|/data/data/com.android.providers.telephony/databases/ 560 // mmssms.db|2.253 ms|SELECT _id, date, message_count, recipient_ids, snippet, snippet_cs, 561 // read, error, has_attachment FROM threads ORDER BY date DESC 562 563 handler.startQuery(token, null, sAllThreadsUri, 564 ALL_THREADS_PROJECTION, selection, null, Conversations.DEFAULT_SORT_ORDER); 565 } 566 567 /** 568 * Start a delete of the conversation with the specified thread ID. 569 * 570 * @param handler An AsyncQueryHandler that will receive onDeleteComplete 571 * upon completion of the conversation being deleted 572 * @param token The token that will be passed to onDeleteComplete 573 * @param deleteAll Delete the whole thread including locked messages 574 * @param threadId Thread ID of the conversation to be deleted 575 */ 576 public static void startDelete(AsyncQueryHandler handler, int token, boolean deleteAll, 577 long threadId) { 578 Uri uri = ContentUris.withAppendedId(Threads.CONTENT_URI, threadId); 579 String selection = deleteAll ? null : "locked=0"; 580 handler.startDelete(token, null, uri, selection, null); 581 } 582 583 /** 584 * Start deleting all conversations in the database. 585 * @param handler An AsyncQueryHandler that will receive onDeleteComplete 586 * upon completion of all conversations being deleted 587 * @param token The token that will be passed to onDeleteComplete 588 * @param deleteAll Delete the whole thread including locked messages 589 */ 590 public static void startDeleteAll(AsyncQueryHandler handler, int token, boolean deleteAll) { 591 String selection = deleteAll ? null : "locked=0"; 592 handler.startDelete(token, null, Threads.CONTENT_URI, selection, null); 593 } 594 595 /** 596 * Check for locked messages in all threads or a specified thread. 597 * @param handler An AsyncQueryHandler that will receive onQueryComplete 598 * upon completion of looking for locked messages 599 * @param threadId The threadId of the thread to search. -1 means all threads 600 * @param token The token that will be passed to onQueryComplete 601 */ 602 public static void startQueryHaveLockedMessages(AsyncQueryHandler handler, long threadId, 603 int token) { 604 handler.cancelOperation(token); 605 Uri uri = MmsSms.CONTENT_LOCKED_URI; 606 if (threadId != -1) { 607 uri = ContentUris.withAppendedId(uri, threadId); 608 } 609 handler.startQuery(token, new Long(threadId), uri, 610 ALL_THREADS_PROJECTION, null, null, Conversations.DEFAULT_SORT_ORDER); 611 } 612 613 /** 614 * Fill the specified conversation with the values from the specified 615 * cursor, possibly setting recipients to empty if {@value allowQuery} 616 * is false and the recipient IDs are not in cache. The cursor should 617 * be one made via {@link #startQueryForAll}. 618 */ 619 private static void fillFromCursor(Context context, Conversation conv, 620 Cursor c, boolean allowQuery) { 621 synchronized (conv) { 622 conv.mThreadId = c.getLong(ID); 623 conv.mDate = c.getLong(DATE); 624 conv.mMessageCount = c.getInt(MESSAGE_COUNT); 625 626 // Replace the snippet with a default value if it's empty. 627 String snippet = MessageUtils.extractEncStrFromCursor(c, SNIPPET, SNIPPET_CS); 628 if (TextUtils.isEmpty(snippet)) { 629 snippet = context.getString(R.string.no_subject_view); 630 } 631 conv.mSnippet = snippet; 632 633 conv.setHasUnreadMessages(c.getInt(READ) == 0); 634 conv.mHasError = (c.getInt(ERROR) != 0); 635 conv.mHasAttachment = (c.getInt(HAS_ATTACHMENT) != 0); 636 } 637 // Fill in as much of the conversation as we can before doing the slow stuff of looking 638 // up the contacts associated with this conversation. 639 String recipientIds = c.getString(RECIPIENT_IDS); 640 ContactList recipients = ContactList.getByIds(recipientIds, allowQuery); 641 synchronized (conv) { 642 conv.mRecipients = recipients; 643 } 644 645 if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) { 646 LogTag.debug("fillFromCursor: conv=" + conv + ", recipientIds=" + recipientIds); 647 } 648 } 649 650 /** 651 * Private cache for the use of the various forms of Conversation.get. 652 */ 653 private static class Cache { 654 private static Cache sInstance = new Cache(); 655 static Cache getInstance() { return sInstance; } 656 private final HashSet<Conversation> mCache; 657 private Cache() { 658 mCache = new HashSet<Conversation>(10); 659 } 660 661 /** 662 * Return the conversation with the specified thread ID, or 663 * null if it's not in cache. 664 */ 665 static Conversation get(long threadId) { 666 synchronized (sInstance) { 667 if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) { 668 LogTag.debug("Conversation get with threadId: " + threadId); 669 } 670 for (Conversation c : sInstance.mCache) { 671 if (DEBUG) { 672 LogTag.debug("Conversation get() threadId: " + threadId + 673 " c.getThreadId(): " + c.getThreadId()); 674 } 675 if (c.getThreadId() == threadId) { 676 return c; 677 } 678 } 679 } 680 return null; 681 } 682 683 /** 684 * Return the conversation with the specified recipient 685 * list, or null if it's not in cache. 686 */ 687 static Conversation get(ContactList list) { 688 synchronized (sInstance) { 689 if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) { 690 LogTag.debug("Conversation get with ContactList: " + list); 691 } 692 for (Conversation c : sInstance.mCache) { 693 if (c.getRecipients().equals(list)) { 694 return c; 695 } 696 } 697 } 698 return null; 699 } 700 701 /** 702 * Put the specified conversation in the cache. The caller 703 * should not place an already-existing conversation in the 704 * cache, but rather update it in place. 705 */ 706 static void put(Conversation c) { 707 synchronized (sInstance) { 708 // We update cache entries in place so people with long- 709 // held references get updated. 710 if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) { 711 LogTag.debug("Conversation.Cache.put: conv= " + c + ", hash: " + c.hashCode()); 712 } 713 714 if (sInstance.mCache.contains(c)) { 715 throw new IllegalStateException("cache already contains " + c + 716 " threadId: " + c.mThreadId); 717 } 718 sInstance.mCache.add(c); 719 } 720 } 721 722 static void remove(long threadId) { 723 synchronized (sInstance) { 724 if (DEBUG) { 725 LogTag.debug("remove threadid: " + threadId); 726 dumpCache(); 727 } 728 for (Conversation c : sInstance.mCache) { 729 if (c.getThreadId() == threadId) { 730 sInstance.mCache.remove(c); 731 return; 732 } 733 } 734 } 735 } 736 737 static void dumpCache() { 738 synchronized (sInstance) { 739 LogTag.debug("Conversation dumpCache: "); 740 for (Conversation c : sInstance.mCache) { 741 LogTag.debug(" conv: " + c.toString() + " hash: " + c.hashCode()); 742 } 743 } 744 } 745 746 /** 747 * Remove all conversations from the cache that are not in 748 * the provided set of thread IDs. 749 */ 750 static void keepOnly(Set<Long> threads) { 751 synchronized (sInstance) { 752 Iterator<Conversation> iter = sInstance.mCache.iterator(); 753 while (iter.hasNext()) { 754 Conversation c = iter.next(); 755 if (!threads.contains(c.getThreadId())) { 756 iter.remove(); 757 } 758 } 759 } 760 if (DEBUG) { 761 LogTag.debug("after keepOnly"); 762 dumpCache(); 763 } 764 } 765 } 766 767 /** 768 * Set up the conversation cache. To be called once at application 769 * startup time. 770 */ 771 public static void init(final Context context) { 772 new Thread(new Runnable() { 773 public void run() { 774 cacheAllThreads(context); 775 } 776 }).start(); 777 } 778 779 public static void markAllConversationsAsSeen(final Context context) { 780 if (DEBUG) { 781 LogTag.debug("Conversation.markAllConversationsAsSeen"); 782 } 783 784 new Thread(new Runnable() { 785 public void run() { 786 blockingMarkAllSmsMessagesAsSeen(context); 787 blockingMarkAllMmsMessagesAsSeen(context); 788 789 // Always update notifications regardless of the read state. 790 MessagingNotification.blockingUpdateAllNotifications(context); 791 } 792 }).start(); 793 } 794 795 private static void blockingMarkAllSmsMessagesAsSeen(final Context context) { 796 ContentResolver resolver = context.getContentResolver(); 797 Cursor cursor = resolver.query(Sms.Inbox.CONTENT_URI, 798 SEEN_PROJECTION, 799 "seen=0", 800 null, 801 null); 802 803 int count = 0; 804 805 if (cursor != null) { 806 try { 807 count = cursor.getCount(); 808 } finally { 809 cursor.close(); 810 } 811 } 812 813 if (count == 0) { 814 return; 815 } 816 817 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 818 Log.d(TAG, "mark " + count + " SMS msgs as seen"); 819 } 820 821 ContentValues values = new ContentValues(1); 822 values.put("seen", 1); 823 824 resolver.update(Sms.Inbox.CONTENT_URI, 825 values, 826 "seen=0", 827 null); 828 } 829 830 private static void blockingMarkAllMmsMessagesAsSeen(final Context context) { 831 ContentResolver resolver = context.getContentResolver(); 832 Cursor cursor = resolver.query(Mms.Inbox.CONTENT_URI, 833 SEEN_PROJECTION, 834 "seen=0", 835 null, 836 null); 837 838 int count = 0; 839 840 if (cursor != null) { 841 try { 842 count = cursor.getCount(); 843 } finally { 844 cursor.close(); 845 } 846 } 847 848 if (count == 0) { 849 return; 850 } 851 852 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 853 Log.d(TAG, "mark " + count + " MMS msgs as seen"); 854 } 855 856 ContentValues values = new ContentValues(1); 857 values.put("seen", 1); 858 859 resolver.update(Mms.Inbox.CONTENT_URI, 860 values, 861 "seen=0", 862 null); 863 864 } 865 866 /** 867 * Are we in the process of loading and caching all the threads?. 868 */ 869 public static boolean loadingThreads() { 870 synchronized (Cache.getInstance()) { 871 return mLoadingThreads; 872 } 873 } 874 875 private static void cacheAllThreads(Context context) { 876 if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) { 877 LogTag.debug("[Conversation] cacheAllThreads: begin"); 878 } 879 synchronized (Cache.getInstance()) { 880 if (mLoadingThreads) { 881 return; 882 } 883 mLoadingThreads = true; 884 } 885 886 // Keep track of what threads are now on disk so we 887 // can discard anything removed from the cache. 888 HashSet<Long> threadsOnDisk = new HashSet<Long>(); 889 890 // Query for all conversations. 891 Cursor c = context.getContentResolver().query(sAllThreadsUri, 892 ALL_THREADS_PROJECTION, null, null, null); 893 try { 894 if (c != null) { 895 while (c.moveToNext()) { 896 long threadId = c.getLong(ID); 897 threadsOnDisk.add(threadId); 898 899 // Try to find this thread ID in the cache. 900 Conversation conv; 901 synchronized (Cache.getInstance()) { 902 conv = Cache.get(threadId); 903 } 904 905 if (conv == null) { 906 // Make a new Conversation and put it in 907 // the cache if necessary. 908 conv = new Conversation(context, c, true); 909 try { 910 synchronized (Cache.getInstance()) { 911 Cache.put(conv); 912 } 913 } catch (IllegalStateException e) { 914 LogTag.error("Tried to add duplicate Conversation to Cache"); 915 } 916 } else { 917 // Or update in place so people with references 918 // to conversations get updated too. 919 fillFromCursor(context, conv, c, true); 920 } 921 } 922 } 923 } finally { 924 if (c != null) { 925 c.close(); 926 } 927 synchronized (Cache.getInstance()) { 928 mLoadingThreads = false; 929 } 930 } 931 932 // Purge the cache of threads that no longer exist on disk. 933 Cache.keepOnly(threadsOnDisk); 934 935 if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) { 936 LogTag.debug("[Conversation] cacheAllThreads: finished"); 937 Cache.dumpCache(); 938 } 939 } 940 941 private boolean loadFromThreadId(long threadId, boolean allowQuery) { 942 Cursor c = mContext.getContentResolver().query(sAllThreadsUri, ALL_THREADS_PROJECTION, 943 "_id=" + Long.toString(threadId), null, null); 944 try { 945 if (c.moveToFirst()) { 946 fillFromCursor(mContext, this, c, allowQuery); 947 948 if (threadId != mThreadId) { 949 LogTag.error("loadFromThreadId: fillFromCursor returned differnt thread_id!" + 950 " threadId=" + threadId + ", mThreadId=" + mThreadId); 951 } 952 } else { 953 LogTag.error("loadFromThreadId: Can't find thread ID " + threadId); 954 return false; 955 } 956 } finally { 957 c.close(); 958 } 959 return true; 960 } 961} 962