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