Conversation.java revision 3b21f6ab04db5936d73e9f53032f1587389380ff
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 516 // This query looks like this in the log: 517 // I/Database( 147): elapsedTime4Sql|/data/data/com.android.providers.telephony/databases/ 518 // mmssms.db|2.253 ms|SELECT _id, date, message_count, recipient_ids, snippet, snippet_cs, 519 // read, error, has_attachment FROM threads ORDER BY date DESC 520 521 handler.startQuery(token, null, sAllThreadsUri, 522 ALL_THREADS_PROJECTION, null, null, Conversations.DEFAULT_SORT_ORDER); 523 } 524 525 /** 526 * Start a delete of the conversation with the specified thread ID. 527 * 528 * @param handler An AsyncQueryHandler that will receive onDeleteComplete 529 * upon completion of the conversation being deleted 530 * @param token The token that will be passed to onDeleteComplete 531 * @param deleteAll Delete the whole thread including locked messages 532 * @param threadId Thread ID of the conversation to be deleted 533 */ 534 public static void startDelete(AsyncQueryHandler handler, int token, boolean deleteAll, 535 long threadId) { 536 Uri uri = ContentUris.withAppendedId(Threads.CONTENT_URI, threadId); 537 String selection = deleteAll ? null : "locked=0"; 538 handler.startDelete(token, null, uri, selection, null); 539 } 540 541 /** 542 * Start deleting all conversations in the database. 543 * @param handler An AsyncQueryHandler that will receive onDeleteComplete 544 * upon completion of all conversations being deleted 545 * @param token The token that will be passed to onDeleteComplete 546 * @param deleteAll Delete the whole thread including locked messages 547 */ 548 public static void startDeleteAll(AsyncQueryHandler handler, int token, boolean deleteAll) { 549 String selection = deleteAll ? null : "locked=0"; 550 handler.startDelete(token, null, Threads.CONTENT_URI, selection, null); 551 } 552 553 /** 554 * Check for locked messages in all threads or a specified thread. 555 * @param handler An AsyncQueryHandler that will receive onQueryComplete 556 * upon completion of looking for locked messages 557 * @param threadId The threadId of the thread to search. -1 means all threads 558 * @param token The token that will be passed to onQueryComplete 559 */ 560 public static void startQueryHaveLockedMessages(AsyncQueryHandler handler, long threadId, 561 int token) { 562 handler.cancelOperation(token); 563 Uri uri = MmsSms.CONTENT_LOCKED_URI; 564 if (threadId != -1) { 565 uri = ContentUris.withAppendedId(uri, threadId); 566 } 567 handler.startQuery(token, new Long(threadId), uri, 568 ALL_THREADS_PROJECTION, null, null, Conversations.DEFAULT_SORT_ORDER); 569 } 570 571 /** 572 * Fill the specified conversation with the values from the specified 573 * cursor, possibly setting recipients to empty if {@value allowQuery} 574 * is false and the recipient IDs are not in cache. The cursor should 575 * be one made via {@link #startQueryForAll}. 576 */ 577 private static void fillFromCursor(Context context, Conversation conv, 578 Cursor c, boolean allowQuery) { 579 synchronized (conv) { 580 conv.mThreadId = c.getLong(ID); 581 conv.mDate = c.getLong(DATE); 582 conv.mMessageCount = c.getInt(MESSAGE_COUNT); 583 584 // Replace the snippet with a default value if it's empty. 585 String snippet = MessageUtils.extractEncStrFromCursor(c, SNIPPET, SNIPPET_CS); 586 if (TextUtils.isEmpty(snippet)) { 587 snippet = context.getString(R.string.no_subject_view); 588 } 589 conv.mSnippet = snippet; 590 591 conv.mHasUnreadMessages = (c.getInt(READ) == 0); 592 conv.mHasError = (c.getInt(ERROR) != 0); 593 conv.mHasAttachment = (c.getInt(HAS_ATTACHMENT) != 0); 594 } 595 // Fill in as much of the conversation as we can before doing the slow stuff of looking 596 // up the contacts associated with this conversation. 597 String recipientIds = c.getString(RECIPIENT_IDS); 598 ContactList recipients = ContactList.getByIds(recipientIds, allowQuery); 599 synchronized (conv) { 600 conv.mRecipients = recipients; 601 } 602 } 603 604 /** 605 * Private cache for the use of the various forms of Conversation.get. 606 */ 607 private static class Cache { 608 private static Cache sInstance = new Cache(); 609 static Cache getInstance() { return sInstance; } 610 private final HashSet<Conversation> mCache; 611 private Cache() { 612 mCache = new HashSet<Conversation>(10); 613 } 614 615 /** 616 * Return the conversation with the specified thread ID, or 617 * null if it's not in cache. 618 */ 619 static Conversation get(long threadId) { 620 synchronized (sInstance) { 621 if (DEBUG) { 622 LogTag.debug("Conversation get with threadId: " + threadId); 623 } 624 dumpCache(); 625 for (Conversation c : sInstance.mCache) { 626 if (DEBUG) { 627 LogTag.debug("Conversation get() threadId: " + threadId + 628 " c.getThreadId(): " + c.getThreadId()); 629 } 630 if (c.getThreadId() == threadId) { 631 return c; 632 } 633 } 634 } 635 return null; 636 } 637 638 /** 639 * Return the conversation with the specified recipient 640 * list, or null if it's not in cache. 641 */ 642 static Conversation get(ContactList list) { 643 synchronized (sInstance) { 644 if (DEBUG) { 645 LogTag.debug("Conversation get with ContactList: " + list); 646 dumpCache(); 647 } 648 for (Conversation c : sInstance.mCache) { 649 if (c.getRecipients().equals(list)) { 650 return c; 651 } 652 } 653 } 654 return null; 655 } 656 657 /** 658 * Put the specified conversation in the cache. The caller 659 * should not place an already-existing conversation in the 660 * cache, but rather update it in place. 661 */ 662 static void put(Conversation c) { 663 synchronized (sInstance) { 664 // We update cache entries in place so people with long- 665 // held references get updated. 666 if (DEBUG) { 667 LogTag.debug("Conversation c: " + c + " put with threadid: " + c.getThreadId() + 668 " c.hash: " + c.hashCode()); 669 dumpCache(); 670 } 671 672 if (sInstance.mCache.contains(c)) { 673 throw new IllegalStateException("cache already contains " + c + 674 " threadId: " + c.mThreadId); 675 } 676 sInstance.mCache.add(c); 677 } 678 } 679 680 static void remove(long threadId) { 681 if (DEBUG) { 682 LogTag.debug("remove threadid: " + threadId); 683 dumpCache(); 684 } 685 for (Conversation c : sInstance.mCache) { 686 if (c.getThreadId() == threadId) { 687 sInstance.mCache.remove(c); 688 return; 689 } 690 } 691 } 692 693 static void dumpCache() { 694 if (DEBUG) { 695 synchronized (sInstance) { 696 LogTag.debug("Conversation dumpCache: "); 697 for (Conversation c : sInstance.mCache) { 698 LogTag.debug(" c: " + c + " c.getThreadId(): " + c.getThreadId() + 699 " hash: " + c.hashCode()); 700 } 701 } 702 } 703 } 704 705 /** 706 * Remove all conversations from the cache that are not in 707 * the provided set of thread IDs. 708 */ 709 static void keepOnly(Set<Long> threads) { 710 synchronized (sInstance) { 711 Iterator<Conversation> iter = sInstance.mCache.iterator(); 712 while (iter.hasNext()) { 713 Conversation c = iter.next(); 714 if (!threads.contains(c.getThreadId())) { 715 iter.remove(); 716 } 717 } 718 } 719 if (DEBUG) { 720 LogTag.debug("after keepOnly"); 721 dumpCache(); 722 } 723 } 724 } 725 726 /** 727 * Set up the conversation cache. To be called once at application 728 * startup time. 729 */ 730 public static void init(final Context context) { 731 new Thread(new Runnable() { 732 public void run() { 733 cacheAllThreads(context); 734 } 735 }).start(); 736 } 737 738 public static void markAllConversationsAsSeen(final Context context) { 739 if (DEBUG) { 740 LogTag.debug("Conversation.markAllConversationsAsSeen"); 741 } 742 743 new Thread(new Runnable() { 744 public void run() { 745 blockingMarkAllSmsMessagesAsSeen(context); 746 blockingMarkAllMmsMessagesAsSeen(context); 747 748 // Always update notifications regardless of the read state. 749 MessagingNotification.blockingUpdateAllNotifications(context); 750 } 751 }).start(); 752 } 753 754 private static void blockingMarkAllSmsMessagesAsSeen(final Context context) { 755 ContentResolver resolver = context.getContentResolver(); 756 Cursor cursor = resolver.query(Sms.Inbox.CONTENT_URI, 757 SEEN_PROJECTION, 758 "seen=0", 759 null, 760 null); 761 762 int count = 0; 763 764 if (cursor != null) { 765 try { 766 count = cursor.getCount(); 767 } finally { 768 cursor.close(); 769 } 770 } 771 772 if (count == 0) { 773 return; 774 } 775 776 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 777 Log.d(TAG, "mark " + count + " SMS msgs as seen"); 778 } 779 780 ContentValues values = new ContentValues(1); 781 values.put("seen", 1); 782 783 resolver.update(Sms.Inbox.CONTENT_URI, 784 values, 785 "seen=0", 786 null); 787 } 788 789 private static void blockingMarkAllMmsMessagesAsSeen(final Context context) { 790 ContentResolver resolver = context.getContentResolver(); 791 Cursor cursor = resolver.query(Mms.Inbox.CONTENT_URI, 792 SEEN_PROJECTION, 793 "seen=0", 794 null, 795 null); 796 797 int count = 0; 798 799 if (cursor != null) { 800 try { 801 count = cursor.getCount(); 802 } finally { 803 cursor.close(); 804 } 805 } 806 807 if (count == 0) { 808 return; 809 } 810 811 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 812 Log.d(TAG, "mark " + count + " MMS msgs as seen"); 813 } 814 815 ContentValues values = new ContentValues(1); 816 values.put("seen", 1); 817 818 resolver.update(Mms.Inbox.CONTENT_URI, 819 values, 820 "seen=0", 821 null); 822 823 } 824 825 /** 826 * Are we in the process of loading and caching all the threads?. 827 */ 828 public static boolean loadingThreads() { 829 synchronized (Cache.getInstance()) { 830 return mLoadingThreads; 831 } 832 } 833 834 private static void cacheAllThreads(Context context) { 835 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 836 LogTag.debug("[Conversation] cacheAllThreads"); 837 } 838 synchronized (Cache.getInstance()) { 839 if (mLoadingThreads) { 840 return; 841 } 842 mLoadingThreads = true; 843 } 844 845 // Keep track of what threads are now on disk so we 846 // can discard anything removed from the cache. 847 HashSet<Long> threadsOnDisk = new HashSet<Long>(); 848 849 // Query for all conversations. 850 Cursor c = context.getContentResolver().query(sAllThreadsUri, 851 ALL_THREADS_PROJECTION, null, null, null); 852 try { 853 if (c != null) { 854 while (c.moveToNext()) { 855 long threadId = c.getLong(ID); 856 threadsOnDisk.add(threadId); 857 858 // Try to find this thread ID in the cache. 859 Conversation conv; 860 synchronized (Cache.getInstance()) { 861 conv = Cache.get(threadId); 862 } 863 864 if (conv == null) { 865 // Make a new Conversation and put it in 866 // the cache if necessary. 867 conv = new Conversation(context, c, true); 868 try { 869 synchronized (Cache.getInstance()) { 870 Cache.put(conv); 871 } 872 } catch (IllegalStateException e) { 873 LogTag.error("Tried to add duplicate Conversation to Cache"); 874 } 875 } else { 876 // Or update in place so people with references 877 // to conversations get updated too. 878 fillFromCursor(context, conv, c, true); 879 } 880 } 881 } 882 } finally { 883 if (c != null) { 884 c.close(); 885 } 886 synchronized (Cache.getInstance()) { 887 mLoadingThreads = false; 888 } 889 } 890 891 // Purge the cache of threads that no longer exist on disk. 892 Cache.keepOnly(threadsOnDisk); 893 } 894 895 private boolean loadFromThreadId(long threadId, boolean allowQuery) { 896 Cursor c = mContext.getContentResolver().query(sAllThreadsUri, ALL_THREADS_PROJECTION, 897 "_id=" + Long.toString(threadId), null, null); 898 try { 899 if (c.moveToFirst()) { 900 fillFromCursor(mContext, this, c, allowQuery); 901 } else { 902 LogTag.error("loadFromThreadId: Can't find thread ID " + threadId); 903 return false; 904 } 905 } finally { 906 c.close(); 907 } 908 return true; 909 } 910} 911