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.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 
    105     private Conversation(Context context) {
    106         mContext = context;
    107         mRecipients = new ContactList();
    108         mThreadId = 0;
    109     }
    110 
    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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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      */
    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      */
    513     public synchronized ContactList getRecipients() {
    514         return mRecipients;
    515     }
    516 
    517     /**
    518      * Returns true if a draft message exists in this conversation.
    519      */
    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      */
    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      */
    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      */
    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      */
    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      */
    563     public synchronized String getSnippet() {
    564         return mSnippet;
    565     }
    566 
    567     /**
    568      * Returns true if there are any unread messages in the conversation.
    569      */
    570     public boolean hasUnreadMessages() {
    571         synchronized (this) {
    572             return mHasUnreadMessages;
    573         }
    574     }
    575 
    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      */
    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      */
    592     public synchronized boolean hasError() {
    593         return mHasError;
    594     }
    595 
    596     /**
    597      * Returns true if this conversation is selected for a multi-operation.
    598      */
    599     public synchronized boolean isChecked() {
    600         return mIsChecked;
    601     }
    602 
    603     public synchronized void setIsChecked(boolean isChecked) {
    604         mIsChecked = isChecked;
    605     }
    606 
    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 
    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
    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
    693     public synchronized int hashCode() {
    694         return mRecipients.hashCode();
    695     }
    696 
    697     @Override
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    816         public ConversationQueryHandler(ContentResolver cr) {
    817             super(cr);
    818         }
    819 
    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
    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      */
    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      */
    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      */
    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();
    942         static Cache getInstance() { return sInstance; }
    943         private final HashSet<Conversation> mCache;
    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          */
    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          */
    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          */
    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          */
   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 
   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 
   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          */
   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      */
   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 
   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 
   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 
   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      */
   1201     public static boolean loadingThreads() {
   1202         synchronized (Cache.getInstance()) {
   1203             return sLoadingThreads;
   1204         }
   1205     }
   1206 
   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 
   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 
   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 
   1304     public static void dump() {
   1305         Cache.dumpCache();
   1306     }
   1307 
   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 
   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      */
   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