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