Conversation.java revision d4e997a8f0e03688143d1a50b381c6f363a204ca
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 mRecipients = list; 395 396 // Invalidate thread ID because the recipient set has changed. 397 mThreadId = 0; 398 } 399 400 /** 401 * Returns the recipient set of this conversation. 402 */ 403 public synchronized ContactList getRecipients() { 404 return mRecipients; 405 } 406 407 /** 408 * Returns true if a draft message exists in this conversation. 409 */ 410 public synchronized boolean hasDraft() { 411 if (mThreadId <= 0) 412 return false; 413 414 return DraftCache.getInstance().hasDraft(mThreadId); 415 } 416 417 /** 418 * Sets whether or not this conversation has a draft message. 419 */ 420 public synchronized void setDraftState(boolean hasDraft) { 421 if (mThreadId <= 0) 422 return; 423 424 DraftCache.getInstance().setDraftState(mThreadId, hasDraft); 425 } 426 427 /** 428 * Returns the time of the last update to this conversation in milliseconds, 429 * on the {@link System#currentTimeMillis} timebase. 430 */ 431 public synchronized long getDate() { 432 return mDate; 433 } 434 435 /** 436 * Returns the number of messages in this conversation, excluding the draft 437 * (if it exists). 438 */ 439 public synchronized int getMessageCount() { 440 return mMessageCount; 441 } 442 /** 443 * Set the number of messages in this conversation, excluding the draft 444 * (if it exists). 445 */ 446 public synchronized void setMessageCount(int cnt) { 447 mMessageCount = cnt; 448 } 449 450 /** 451 * Returns a snippet of text from the most recent message in the conversation. 452 */ 453 public synchronized String getSnippet() { 454 return mSnippet; 455 } 456 457 /** 458 * Returns true if there are any unread messages in the conversation. 459 */ 460 public boolean hasUnreadMessages() { 461 synchronized (this) { 462 return mHasUnreadMessages; 463 } 464 } 465 466 private void setHasUnreadMessages(boolean flag) { 467 synchronized (this) { 468 mHasUnreadMessages = flag; 469 } 470 } 471 472 /** 473 * Returns true if any messages in the conversation have attachments. 474 */ 475 public synchronized boolean hasAttachment() { 476 return mHasAttachment; 477 } 478 479 /** 480 * Returns true if any messages in the conversation are in an error state. 481 */ 482 public synchronized boolean hasError() { 483 return mHasError; 484 } 485 486 /** 487 * Returns true if this conversation is selected for a multi-operation. 488 */ 489 public synchronized boolean isChecked() { 490 return mIsChecked; 491 } 492 493 public synchronized void setIsChecked(boolean isChecked) { 494 mIsChecked = isChecked; 495 } 496 497 private static long getOrCreateThreadId(Context context, ContactList list) { 498 HashSet<String> recipients = new HashSet<String>(); 499 Contact cacheContact = null; 500 for (Contact c : list) { 501 cacheContact = Contact.get(c.getNumber(), false); 502 if (cacheContact != null) { 503 recipients.add(cacheContact.getNumber()); 504 } else { 505 recipients.add(c.getNumber()); 506 } 507 } 508 long retVal = Threads.getOrCreateThreadId(context, recipients); 509 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 510 LogTag.debug("[Conversation] getOrCreateThreadId for (%s) returned %d", 511 recipients, retVal); 512 } 513 514 return retVal; 515 } 516 517 /* 518 * The primary key of a conversation is its recipient set; override 519 * equals() and hashCode() to just pass through to the internal 520 * recipient sets. 521 */ 522 @Override 523 public synchronized boolean equals(Object obj) { 524 try { 525 Conversation other = (Conversation)obj; 526 return (mRecipients.equals(other.mRecipients)); 527 } catch (ClassCastException e) { 528 return false; 529 } 530 } 531 532 @Override 533 public synchronized int hashCode() { 534 return mRecipients.hashCode(); 535 } 536 537 @Override 538 public synchronized String toString() { 539 return String.format("[%s] (tid %d)", mRecipients.serialize(), mThreadId); 540 } 541 542 /** 543 * Remove any obsolete conversations sitting around on disk. Obsolete threads are threads 544 * that aren't referenced by any message in the pdu or sms tables. 545 */ 546 public static void asyncDeleteObsoleteThreads(AsyncQueryHandler handler, int token) { 547 handler.startDelete(token, null, Threads.OBSOLETE_THREADS_URI, null, null); 548 } 549 550 /** 551 * Start a query for all conversations in the database on the specified 552 * AsyncQueryHandler. 553 * 554 * @param handler An AsyncQueryHandler that will receive onQueryComplete 555 * upon completion of the query 556 * @param token The token that will be passed to onQueryComplete 557 */ 558 public static void startQueryForAll(AsyncQueryHandler handler, int token) { 559 handler.cancelOperation(token); 560 561 // This query looks like this in the log: 562 // I/Database( 147): elapsedTime4Sql|/data/data/com.android.providers.telephony/databases/ 563 // mmssms.db|2.253 ms|SELECT _id, date, message_count, recipient_ids, snippet, snippet_cs, 564 // read, error, has_attachment FROM threads ORDER BY date DESC 565 566 startQuery(handler, token, null); 567 } 568 569 /** 570 * Start a query for in the database on the specified AsyncQueryHandler with the specified 571 * "where" clause. 572 * 573 * @param handler An AsyncQueryHandler that will receive onQueryComplete 574 * upon completion of the query 575 * @param token The token that will be passed to onQueryComplete 576 * @param selection A where clause (can be null) to select particular conv items. 577 */ 578 public static void startQuery(AsyncQueryHandler handler, int token, String selection) { 579 handler.cancelOperation(token); 580 581 // This query looks like this in the log: 582 // I/Database( 147): elapsedTime4Sql|/data/data/com.android.providers.telephony/databases/ 583 // mmssms.db|2.253 ms|SELECT _id, date, message_count, recipient_ids, snippet, snippet_cs, 584 // read, error, has_attachment FROM threads ORDER BY date DESC 585 586 handler.startQuery(token, null, sAllThreadsUri, 587 ALL_THREADS_PROJECTION, selection, null, Conversations.DEFAULT_SORT_ORDER); 588 } 589 590 /** 591 * Start a delete of the conversation with the specified thread ID. 592 * 593 * @param handler An AsyncQueryHandler that will receive onDeleteComplete 594 * upon completion of the conversation being deleted 595 * @param token The token that will be passed to onDeleteComplete 596 * @param deleteAll Delete the whole thread including locked messages 597 * @param threadId Thread ID of the conversation to be deleted 598 */ 599 public static void startDelete(AsyncQueryHandler handler, int token, boolean deleteAll, 600 long threadId) { 601 Uri uri = ContentUris.withAppendedId(Threads.CONTENT_URI, threadId); 602 String selection = deleteAll ? null : "locked=0"; 603 handler.startDelete(token, null, uri, selection, null); 604 } 605 606 /** 607 * Start deleting all conversations in the database. 608 * @param handler An AsyncQueryHandler that will receive onDeleteComplete 609 * upon completion of all conversations being deleted 610 * @param token The token that will be passed to onDeleteComplete 611 * @param deleteAll Delete the whole thread including locked messages 612 */ 613 public static void startDeleteAll(AsyncQueryHandler handler, int token, boolean deleteAll) { 614 String selection = deleteAll ? null : "locked=0"; 615 handler.startDelete(token, null, Threads.CONTENT_URI, selection, null); 616 } 617 618 /** 619 * Check for locked messages in all threads or a specified thread. 620 * @param handler An AsyncQueryHandler that will receive onQueryComplete 621 * upon completion of looking for locked messages 622 * @param threadIds A list of threads to search. null means all threads 623 * @param token The token that will be passed to onQueryComplete 624 */ 625 public static void startQueryHaveLockedMessages(AsyncQueryHandler handler, 626 ArrayList<Long> threadIds, 627 int token) { 628 handler.cancelOperation(token); 629 Uri uri = MmsSms.CONTENT_LOCKED_URI; 630 631 String selection = null; 632 if (threadIds != null) { 633 StringBuilder buf = new StringBuilder(); 634 int i = 0; 635 636 for (long threadId : threadIds) { 637 if (i++ > 0) { 638 buf.append(" OR "); 639 } 640 // We have to build the selection arg into the selection because deep down in 641 // provider, the function buildUnionSubQuery takes selectionArgs, but ignores it. 642 buf.append(Mms.THREAD_ID).append("=").append(Long.toString(threadId)); 643 } 644 selection = buf.toString(); 645 } 646 handler.startQuery(token, threadIds, uri, 647 ALL_THREADS_PROJECTION, selection, null, Conversations.DEFAULT_SORT_ORDER); 648 } 649 650 /** 651 * Check for locked messages in all threads or a specified thread. 652 * @param handler An AsyncQueryHandler that will receive onQueryComplete 653 * upon completion of looking for locked messages 654 * @param threadId The threadId of the thread to search. -1 means all threads 655 * @param token The token that will be passed to onQueryComplete 656 */ 657 public static void startQueryHaveLockedMessages(AsyncQueryHandler handler, 658 long threadId, 659 int token) { 660 ArrayList<Long> threadIds = null; 661 if (threadId != -1) { 662 threadIds = new ArrayList<Long>(); 663 threadIds.add(threadId); 664 } 665 startQueryHaveLockedMessages(handler, threadIds, token); 666 } 667 668 /** 669 * Fill the specified conversation with the values from the specified 670 * cursor, possibly setting recipients to empty if {@value allowQuery} 671 * is false and the recipient IDs are not in cache. The cursor should 672 * be one made via {@link #startQueryForAll}. 673 */ 674 private static void fillFromCursor(Context context, Conversation conv, 675 Cursor c, boolean allowQuery) { 676 synchronized (conv) { 677 conv.mThreadId = c.getLong(ID); 678 conv.mDate = c.getLong(DATE); 679 conv.mMessageCount = c.getInt(MESSAGE_COUNT); 680 681 // Replace the snippet with a default value if it's empty. 682 String snippet = MessageUtils.extractEncStrFromCursor(c, SNIPPET, SNIPPET_CS); 683 if (TextUtils.isEmpty(snippet)) { 684 snippet = context.getString(R.string.no_subject_view); 685 } 686 conv.mSnippet = snippet; 687 688 conv.setHasUnreadMessages(c.getInt(READ) == 0); 689 conv.mHasError = (c.getInt(ERROR) != 0); 690 conv.mHasAttachment = (c.getInt(HAS_ATTACHMENT) != 0); 691 } 692 // Fill in as much of the conversation as we can before doing the slow stuff of looking 693 // up the contacts associated with this conversation. 694 String recipientIds = c.getString(RECIPIENT_IDS); 695 ContactList recipients = ContactList.getByIds(recipientIds, allowQuery); 696 synchronized (conv) { 697 conv.mRecipients = recipients; 698 } 699 700 if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) { 701 LogTag.debug("fillFromCursor: conv=" + conv + ", recipientIds=" + recipientIds); 702 } 703 } 704 705 /** 706 * Private cache for the use of the various forms of Conversation.get. 707 */ 708 private static class Cache { 709 private static Cache sInstance = new Cache(); 710 static Cache getInstance() { return sInstance; } 711 private final HashSet<Conversation> mCache; 712 private Cache() { 713 mCache = new HashSet<Conversation>(10); 714 } 715 716 /** 717 * Return the conversation with the specified thread ID, or 718 * null if it's not in cache. 719 */ 720 static Conversation get(long threadId) { 721 synchronized (sInstance) { 722 if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) { 723 LogTag.debug("Conversation get with threadId: " + threadId); 724 } 725 for (Conversation c : sInstance.mCache) { 726 if (DEBUG) { 727 LogTag.debug("Conversation get() threadId: " + threadId + 728 " c.getThreadId(): " + c.getThreadId()); 729 } 730 if (c.getThreadId() == threadId) { 731 return c; 732 } 733 } 734 } 735 return null; 736 } 737 738 /** 739 * Return the conversation with the specified recipient 740 * list, or null if it's not in cache. 741 */ 742 static Conversation get(ContactList list) { 743 synchronized (sInstance) { 744 if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) { 745 LogTag.debug("Conversation get with ContactList: " + list); 746 } 747 for (Conversation c : sInstance.mCache) { 748 if (c.getRecipients().equals(list)) { 749 return c; 750 } 751 } 752 } 753 return null; 754 } 755 756 /** 757 * Put the specified conversation in the cache. The caller 758 * should not place an already-existing conversation in the 759 * cache, but rather update it in place. 760 */ 761 static void put(Conversation c) { 762 synchronized (sInstance) { 763 // We update cache entries in place so people with long- 764 // held references get updated. 765 if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) { 766 LogTag.debug("Conversation.Cache.put: conv= " + c + ", hash: " + c.hashCode()); 767 } 768 769 if (sInstance.mCache.contains(c)) { 770 throw new IllegalStateException("cache already contains " + c + 771 " threadId: " + c.mThreadId); 772 } 773 sInstance.mCache.add(c); 774 } 775 } 776 777 static void remove(long threadId) { 778 synchronized (sInstance) { 779 if (DEBUG) { 780 LogTag.debug("remove threadid: " + threadId); 781 dumpCache(); 782 } 783 for (Conversation c : sInstance.mCache) { 784 if (c.getThreadId() == threadId) { 785 sInstance.mCache.remove(c); 786 return; 787 } 788 } 789 } 790 } 791 792 static void dumpCache() { 793 synchronized (sInstance) { 794 LogTag.debug("Conversation dumpCache: "); 795 for (Conversation c : sInstance.mCache) { 796 LogTag.debug(" conv: " + c.toString() + " hash: " + c.hashCode()); 797 } 798 } 799 } 800 801 /** 802 * Remove all conversations from the cache that are not in 803 * the provided set of thread IDs. 804 */ 805 static void keepOnly(Set<Long> threads) { 806 synchronized (sInstance) { 807 Iterator<Conversation> iter = sInstance.mCache.iterator(); 808 while (iter.hasNext()) { 809 Conversation c = iter.next(); 810 if (!threads.contains(c.getThreadId())) { 811 iter.remove(); 812 } 813 } 814 } 815 if (DEBUG) { 816 LogTag.debug("after keepOnly"); 817 dumpCache(); 818 } 819 } 820 } 821 822 /** 823 * Set up the conversation cache. To be called once at application 824 * startup time. 825 */ 826 public static void init(final Context context) { 827 new Thread(new Runnable() { 828 public void run() { 829 cacheAllThreads(context); 830 } 831 }).start(); 832 } 833 834 public static void markAllConversationsAsSeen(final Context context) { 835 if (DEBUG) { 836 LogTag.debug("Conversation.markAllConversationsAsSeen"); 837 } 838 839 new Thread(new Runnable() { 840 public void run() { 841 blockingMarkAllSmsMessagesAsSeen(context); 842 blockingMarkAllMmsMessagesAsSeen(context); 843 844 // Always update notifications regardless of the read state. 845 MessagingNotification.blockingUpdateAllNotifications(context); 846 } 847 }).start(); 848 } 849 850 private static void blockingMarkAllSmsMessagesAsSeen(final Context context) { 851 ContentResolver resolver = context.getContentResolver(); 852 Cursor cursor = resolver.query(Sms.Inbox.CONTENT_URI, 853 SEEN_PROJECTION, 854 "seen=0", 855 null, 856 null); 857 858 int count = 0; 859 860 if (cursor != null) { 861 try { 862 count = cursor.getCount(); 863 } finally { 864 cursor.close(); 865 } 866 } 867 868 if (count == 0) { 869 return; 870 } 871 872 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 873 Log.d(TAG, "mark " + count + " SMS msgs as seen"); 874 } 875 876 ContentValues values = new ContentValues(1); 877 values.put("seen", 1); 878 879 resolver.update(Sms.Inbox.CONTENT_URI, 880 values, 881 "seen=0", 882 null); 883 } 884 885 private static void blockingMarkAllMmsMessagesAsSeen(final Context context) { 886 ContentResolver resolver = context.getContentResolver(); 887 Cursor cursor = resolver.query(Mms.Inbox.CONTENT_URI, 888 SEEN_PROJECTION, 889 "seen=0", 890 null, 891 null); 892 893 int count = 0; 894 895 if (cursor != null) { 896 try { 897 count = cursor.getCount(); 898 } finally { 899 cursor.close(); 900 } 901 } 902 903 if (count == 0) { 904 return; 905 } 906 907 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 908 Log.d(TAG, "mark " + count + " MMS msgs as seen"); 909 } 910 911 ContentValues values = new ContentValues(1); 912 values.put("seen", 1); 913 914 resolver.update(Mms.Inbox.CONTENT_URI, 915 values, 916 "seen=0", 917 null); 918 919 } 920 921 /** 922 * Are we in the process of loading and caching all the threads?. 923 */ 924 public static boolean loadingThreads() { 925 synchronized (Cache.getInstance()) { 926 return mLoadingThreads; 927 } 928 } 929 930 private static void cacheAllThreads(Context context) { 931 if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) { 932 LogTag.debug("[Conversation] cacheAllThreads: begin"); 933 } 934 synchronized (Cache.getInstance()) { 935 if (mLoadingThreads) { 936 return; 937 } 938 mLoadingThreads = true; 939 } 940 941 // Keep track of what threads are now on disk so we 942 // can discard anything removed from the cache. 943 HashSet<Long> threadsOnDisk = new HashSet<Long>(); 944 945 // Query for all conversations. 946 Cursor c = context.getContentResolver().query(sAllThreadsUri, 947 ALL_THREADS_PROJECTION, null, null, null); 948 try { 949 if (c != null) { 950 while (c.moveToNext()) { 951 long threadId = c.getLong(ID); 952 threadsOnDisk.add(threadId); 953 954 // Try to find this thread ID in the cache. 955 Conversation conv; 956 synchronized (Cache.getInstance()) { 957 conv = Cache.get(threadId); 958 } 959 960 if (conv == null) { 961 // Make a new Conversation and put it in 962 // the cache if necessary. 963 conv = new Conversation(context, c, true); 964 try { 965 synchronized (Cache.getInstance()) { 966 Cache.put(conv); 967 } 968 } catch (IllegalStateException e) { 969 LogTag.error("Tried to add duplicate Conversation to Cache"); 970 } 971 } else { 972 // Or update in place so people with references 973 // to conversations get updated too. 974 fillFromCursor(context, conv, c, true); 975 } 976 } 977 } 978 } finally { 979 if (c != null) { 980 c.close(); 981 } 982 synchronized (Cache.getInstance()) { 983 mLoadingThreads = false; 984 } 985 } 986 987 // Purge the cache of threads that no longer exist on disk. 988 Cache.keepOnly(threadsOnDisk); 989 990 if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) { 991 LogTag.debug("[Conversation] cacheAllThreads: finished"); 992 Cache.dumpCache(); 993 } 994 } 995 996 private boolean loadFromThreadId(long threadId, boolean allowQuery) { 997 Cursor c = mContext.getContentResolver().query(sAllThreadsUri, ALL_THREADS_PROJECTION, 998 "_id=" + Long.toString(threadId), null, null); 999 try { 1000 if (c.moveToFirst()) { 1001 fillFromCursor(mContext, this, c, allowQuery); 1002 1003 if (threadId != mThreadId) { 1004 LogTag.error("loadFromThreadId: fillFromCursor returned differnt thread_id!" + 1005 " threadId=" + threadId + ", mThreadId=" + mThreadId); 1006 } 1007 } else { 1008 LogTag.error("loadFromThreadId: Can't find thread ID " + threadId); 1009 return false; 1010 } 1011 } finally { 1012 c.close(); 1013 } 1014 return true; 1015 } 1016 1017 public static String getRecipients(Uri uri) { 1018 String base = uri.getSchemeSpecificPart(); 1019 int pos = base.indexOf('?'); 1020 return (pos == -1) ? base : base.substring(0, pos); 1021 } 1022 1023 public static void dump() { 1024 Cache.dumpCache(); 1025 } 1026 1027 public static void dumpThreadsTable(Context context) { 1028 LogTag.debug("**** Dump of threads table ****"); 1029 Cursor c = context.getContentResolver().query(sAllThreadsUri, 1030 ALL_THREADS_PROJECTION, null, null, "date ASC"); 1031 try { 1032 c.moveToPosition(-1); 1033 while (c.moveToNext()) { 1034 String snippet = MessageUtils.extractEncStrFromCursor(c, SNIPPET, SNIPPET_CS); 1035 LogTag.debug("dumpThreadsTable threadId: " + c.getLong(ID) + 1036 " " + ThreadsColumns.DATE + " : " + c.getLong(DATE) + 1037 " " + ThreadsColumns.MESSAGE_COUNT + " : " + c.getInt(MESSAGE_COUNT) + 1038 " " + ThreadsColumns.SNIPPET + " : " + snippet + 1039 " " + ThreadsColumns.READ + " : " + c.getInt(READ) + 1040 " " + ThreadsColumns.ERROR + " : " + c.getInt(ERROR) + 1041 " " + ThreadsColumns.HAS_ATTACHMENT + " : " + c.getInt(HAS_ATTACHMENT) + 1042 " " + ThreadsColumns.RECIPIENT_IDS + " : " + c.getString(RECIPIENT_IDS)); 1043 1044 ContactList recipients = ContactList.getByIds(c.getString(RECIPIENT_IDS), false); 1045 LogTag.debug("----recipients: " + recipients.serialize()); 1046 } 1047 } finally { 1048 c.close(); 1049 } 1050 } 1051 1052 static final String[] SMS_PROJECTION = new String[] { 1053 BaseColumns._ID, 1054 // For SMS 1055 Sms.THREAD_ID, 1056 Sms.ADDRESS, 1057 Sms.BODY, 1058 Sms.DATE, 1059 Sms.READ, 1060 Sms.TYPE, 1061 Sms.STATUS, 1062 Sms.LOCKED, 1063 Sms.ERROR_CODE, 1064 }; 1065 1066 // The indexes of the default columns which must be consistent 1067 // with above PROJECTION. 1068 static final int COLUMN_ID = 0; 1069 static final int COLUMN_THREAD_ID = 1; 1070 static final int COLUMN_SMS_ADDRESS = 2; 1071 static final int COLUMN_SMS_BODY = 3; 1072 static final int COLUMN_SMS_DATE = 4; 1073 static final int COLUMN_SMS_READ = 5; 1074 static final int COLUMN_SMS_TYPE = 6; 1075 static final int COLUMN_SMS_STATUS = 7; 1076 static final int COLUMN_SMS_LOCKED = 8; 1077 static final int COLUMN_SMS_ERROR_CODE = 9; 1078 1079 public static void dumpSmsTable(Context context) { 1080 LogTag.debug("**** Dump of sms table ****"); 1081 Cursor c = context.getContentResolver().query(Sms.CONTENT_URI, 1082 SMS_PROJECTION, null, null, "_id DESC"); 1083 try { 1084 // Only dump the latest 20 messages 1085 c.moveToPosition(-1); 1086 while (c.moveToNext() && c.getPosition() < 20) { 1087 String body = c.getString(COLUMN_SMS_BODY); 1088 LogTag.debug("dumpSmsTable " + BaseColumns._ID + ": " + c.getLong(COLUMN_ID) + 1089 " " + Sms.THREAD_ID + " : " + c.getLong(DATE) + 1090 " " + Sms.ADDRESS + " : " + c.getString(COLUMN_SMS_ADDRESS) + 1091 " " + Sms.BODY + " : " + body.substring(0, Math.min(body.length(), 8)) + 1092 " " + Sms.DATE + " : " + c.getLong(COLUMN_SMS_DATE) + 1093 " " + Sms.TYPE + " : " + c.getInt(COLUMN_SMS_TYPE)); 1094 } 1095 } finally { 1096 c.close(); 1097 } 1098 } 1099 1100} 1101