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