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