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