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