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