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