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