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