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