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