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