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