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