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