Home | History | Annotate | Download | only in data
      1 package com.android.mms.data;
      2 
      3 import java.util.HashSet;
      4 import java.util.Iterator;
      5 import java.util.Set;
      6 
      7 import android.content.AsyncQueryHandler;
      8 import android.content.ContentResolver;
      9 import android.content.ContentUris;
     10 import android.content.ContentValues;
     11 import android.content.Context;
     12 import android.database.Cursor;
     13 import android.net.Uri;
     14 import android.provider.Telephony.Mms;
     15 import android.provider.Telephony.MmsSms;
     16 import android.provider.Telephony.Sms;
     17 import android.provider.Telephony.Threads;
     18 import android.provider.Telephony.Sms.Conversations;
     19 import android.text.TextUtils;
     20 import android.util.Log;
     21 
     22 import com.android.mms.LogTag;
     23 import com.android.mms.R;
     24 import com.android.mms.transaction.MessagingNotification;
     25 import com.android.mms.ui.MessageUtils;
     26 import com.android.mms.util.DraftCache;
     27 
     28 /**
     29  * An interface for finding information about conversations and/or creating new ones.
     30  */
     31 public class Conversation {
     32     private static final String TAG = "Mms/conv";
     33     private static final boolean DEBUG = false;
     34 
     35     private static final Uri sAllThreadsUri =
     36         Threads.CONTENT_URI.buildUpon().appendQueryParameter("simple", "true").build();
     37 
     38     private static final String[] ALL_THREADS_PROJECTION = {
     39         Threads._ID, Threads.DATE, Threads.MESSAGE_COUNT, Threads.RECIPIENT_IDS,
     40         Threads.SNIPPET, Threads.SNIPPET_CHARSET, Threads.READ, Threads.ERROR,
     41         Threads.HAS_ATTACHMENT
     42     };
     43 
     44     private static final String[] UNREAD_PROJECTION = {
     45         Threads._ID,
     46         Threads.READ
     47     };
     48 
     49     private static final String UNREAD_SELECTION = "(read=0 OR seen=0)";
     50 
     51     private static final String[] SEEN_PROJECTION = new String[] {
     52         "seen"
     53     };
     54 
     55     private static final int ID             = 0;
     56     private static final int DATE           = 1;
     57     private static final int MESSAGE_COUNT  = 2;
     58     private static final int RECIPIENT_IDS  = 3;
     59     private static final int SNIPPET        = 4;
     60     private static final int SNIPPET_CS     = 5;
     61     private static final int READ           = 6;
     62     private static final int ERROR          = 7;
     63     private static final int HAS_ATTACHMENT = 8;
     64 
     65 
     66     private final Context mContext;
     67 
     68     // The thread ID of this conversation.  Can be zero in the case of a
     69     // new conversation where the recipient set is changing as the user
     70     // types and we have not hit the database yet to create a thread.
     71     private long mThreadId;
     72 
     73     private ContactList mRecipients;    // The current set of recipients.
     74     private long mDate;                 // The last update time.
     75     private int mMessageCount;          // Number of messages.
     76     private String mSnippet;            // Text of the most recent message.
     77     private boolean mHasUnreadMessages; // True if there are unread messages.
     78     private boolean mHasAttachment;     // True if any message has an attachment.
     79     private boolean mHasError;          // True if any message is in an error state.
     80 
     81     private static ContentValues mReadContentValues;
     82     private static boolean mLoadingThreads;
     83     private boolean mMarkAsReadBlocked;
     84     private Object mMarkAsBlockedSyncer = new Object();
     85 
     86     private Conversation(Context context) {
     87         mContext = context;
     88         mRecipients = new ContactList();
     89         mThreadId = 0;
     90     }
     91 
     92     private Conversation(Context context, long threadId, boolean allowQuery) {
     93         mContext = context;
     94         if (!loadFromThreadId(threadId, allowQuery)) {
     95             mRecipients = new ContactList();
     96             mThreadId = 0;
     97         }
     98     }
     99 
    100     private Conversation(Context context, Cursor cursor, boolean allowQuery) {
    101         mContext = context;
    102         fillFromCursor(context, this, cursor, allowQuery);
    103     }
    104 
    105     /**
    106      * Create a new conversation with no recipients.  {@link #setRecipients} can
    107      * be called as many times as you like; the conversation will not be
    108      * created in the database until {@link #ensureThreadId} is called.
    109      */
    110     public static Conversation createNew(Context context) {
    111         return new Conversation(context);
    112     }
    113 
    114     /**
    115      * Find the conversation matching the provided thread ID.
    116      */
    117     public static Conversation get(Context context, long threadId, boolean allowQuery) {
    118         Conversation conv = Cache.get(threadId);
    119         if (conv != null)
    120             return conv;
    121 
    122         conv = new Conversation(context, threadId, allowQuery);
    123         try {
    124             Cache.put(conv);
    125         } catch (IllegalStateException e) {
    126             LogTag.error("Tried to add duplicate Conversation to Cache");
    127         }
    128         return conv;
    129     }
    130 
    131     /**
    132      * Find the conversation matching the provided recipient set.
    133      * When called with an empty recipient list, equivalent to {@link #createNew}.
    134      */
    135     public static Conversation get(Context context, ContactList recipients, boolean allowQuery) {
    136         // If there are no recipients in the list, make a new conversation.
    137         if (recipients.size() < 1) {
    138             return createNew(context);
    139         }
    140 
    141         Conversation conv = Cache.get(recipients);
    142         if (conv != null)
    143             return conv;
    144 
    145         long threadId = getOrCreateThreadId(context, recipients);
    146         conv = new Conversation(context, threadId, allowQuery);
    147         Log.d(TAG, "Conversation.get: created new conversation " + /*conv.toString()*/ "xxxxxxx");
    148 
    149         if (!conv.getRecipients().equals(recipients)) {
    150             Log.e(TAG, "Conversation.get: new conv's recipients don't match input recpients "
    151                     + /*recipients*/ "xxxxxxx");
    152         }
    153 
    154         try {
    155             Cache.put(conv);
    156         } catch (IllegalStateException e) {
    157             LogTag.error("Tried to add duplicate Conversation to Cache");
    158         }
    159 
    160         return conv;
    161     }
    162 
    163     /**
    164      * Find the conversation matching in the specified Uri.  Example
    165      * forms: {@value content://mms-sms/conversations/3} or
    166      * {@value sms:+12124797990}.
    167      * When called with a null Uri, equivalent to {@link #createNew}.
    168      */
    169     public static Conversation get(Context context, Uri uri, boolean allowQuery) {
    170         if (uri == null) {
    171             return createNew(context);
    172         }
    173 
    174         if (DEBUG) Log.v(TAG, "Conversation get URI: " + uri);
    175 
    176         // Handle a conversation URI
    177         if (uri.getPathSegments().size() >= 2) {
    178             try {
    179                 long threadId = Long.parseLong(uri.getPathSegments().get(1));
    180                 if (DEBUG) {
    181                     Log.v(TAG, "Conversation get threadId: " + threadId);
    182                 }
    183                 return get(context, threadId, allowQuery);
    184             } catch (NumberFormatException exception) {
    185                 LogTag.error("Invalid URI: " + uri);
    186             }
    187         }
    188 
    189         String recipient = getRecipients(uri);
    190         return get(context, ContactList.getByNumbers(recipient,
    191                 allowQuery /* don't block */, true /* replace number */), allowQuery);
    192     }
    193 
    194     /**
    195      * Returns true if the recipient in the uri matches the recipient list in this
    196      * conversation.
    197      */
    198     public boolean sameRecipient(Uri uri) {
    199         int size = mRecipients.size();
    200         if (size > 1) {
    201             return false;
    202         }
    203         if (uri == null) {
    204             return size == 0;
    205         }
    206         if (uri.getPathSegments().size() >= 2) {
    207             return false;       // it's a thread id for a conversation
    208         }
    209         String recipient = getRecipients(uri);
    210         ContactList incomingRecipient = ContactList.getByNumbers(recipient,
    211                 false /* don't block */, false /* don't replace number */);
    212         return mRecipients.equals(incomingRecipient);
    213     }
    214 
    215     /**
    216      * Returns a temporary Conversation (not representing one on disk) wrapping
    217      * the contents of the provided cursor.  The cursor should be the one
    218      * returned to your AsyncQueryHandler passed in to {@link #startQueryForAll}.
    219      * The recipient list of this conversation can be empty if the results
    220      * were not in cache.
    221      */
    222     public static Conversation from(Context context, Cursor cursor) {
    223         // First look in the cache for the Conversation and return that one. That way, all the
    224         // people that are looking at the cached copy will get updated when fillFromCursor() is
    225         // called with this cursor.
    226         long threadId = cursor.getLong(ID);
    227         if (threadId > 0) {
    228             Conversation conv = Cache.get(threadId);
    229             if (conv != null) {
    230                 fillFromCursor(context, conv, cursor, false);   // update the existing conv in-place
    231                 return conv;
    232             }
    233         }
    234         Conversation conv = new Conversation(context, cursor, false);
    235         try {
    236             Cache.put(conv);
    237         } catch (IllegalStateException e) {
    238             LogTag.error("Tried to add duplicate Conversation to Cache");
    239         }
    240         return conv;
    241     }
    242 
    243     private void buildReadContentValues() {
    244         if (mReadContentValues == null) {
    245             mReadContentValues = new ContentValues(2);
    246             mReadContentValues.put("read", 1);
    247             mReadContentValues.put("seen", 1);
    248         }
    249     }
    250 
    251     /**
    252      * Marks all messages in this conversation as read and updates
    253      * relevant notifications.  This method returns immediately;
    254      * work is dispatched to a background thread.
    255      */
    256     public void markAsRead() {
    257         // If we have no Uri to mark (as in the case of a conversation that
    258         // has not yet made its way to disk), there's nothing to do.
    259         final Uri threadUri = getUri();
    260 
    261         new Thread(new Runnable() {
    262             public void run() {
    263                 synchronized(mMarkAsBlockedSyncer) {
    264                     if (mMarkAsReadBlocked) {
    265                         try {
    266                             mMarkAsBlockedSyncer.wait();
    267                         } catch (InterruptedException e) {
    268                         }
    269                     }
    270 
    271                     if (threadUri != null) {
    272                         buildReadContentValues();
    273 
    274                         // Check the read flag first. It's much faster to do a query than
    275                         // to do an update. Timing this function show it's about 10x faster to
    276                         // do the query compared to the update, even when there's nothing to
    277                         // update.
    278                         boolean needUpdate = true;
    279 
    280                         Cursor c = mContext.getContentResolver().query(threadUri,
    281                                 UNREAD_PROJECTION, UNREAD_SELECTION, null, null);
    282                         if (c != null) {
    283                             try {
    284                                 needUpdate = c.getCount() > 0;
    285                             } finally {
    286                                 c.close();
    287                             }
    288                         }
    289 
    290                         if (needUpdate) {
    291                             LogTag.debug("markAsRead: update read/seen for thread uri: " +
    292                                     threadUri);
    293                             mContext.getContentResolver().update(threadUri, mReadContentValues,
    294                                     UNREAD_SELECTION, null);
    295                         }
    296 
    297                         setHasUnreadMessages(false);
    298                     }
    299                 }
    300 
    301                 // Always update notifications regardless of the read state.
    302                 MessagingNotification.blockingUpdateAllNotifications(mContext);
    303             }
    304         }).start();
    305     }
    306 
    307     public void blockMarkAsRead(boolean block) {
    308         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    309             LogTag.debug("blockMarkAsRead: " + block);
    310         }
    311 
    312         synchronized(mMarkAsBlockedSyncer) {
    313             if (block != mMarkAsReadBlocked) {
    314                 mMarkAsReadBlocked = block;
    315                 if (!mMarkAsReadBlocked) {
    316                     mMarkAsBlockedSyncer.notifyAll();
    317                 }
    318             }
    319 
    320         }
    321     }
    322 
    323     /**
    324      * Returns a content:// URI referring to this conversation,
    325      * or null if it does not exist on disk yet.
    326      */
    327     public synchronized Uri getUri() {
    328         if (mThreadId <= 0)
    329             return null;
    330 
    331         return ContentUris.withAppendedId(Threads.CONTENT_URI, mThreadId);
    332     }
    333 
    334     /**
    335      * Return the Uri for all messages in the given thread ID.
    336      * @deprecated
    337      */
    338     public static Uri getUri(long threadId) {
    339         // TODO: Callers using this should really just have a Conversation
    340         // and call getUri() on it, but this guarantees no blocking.
    341         return ContentUris.withAppendedId(Threads.CONTENT_URI, threadId);
    342     }
    343 
    344     /**
    345      * Returns the thread ID of this conversation.  Can be zero if
    346      * {@link #ensureThreadId} has not been called yet.
    347      */
    348     public synchronized long getThreadId() {
    349         return mThreadId;
    350     }
    351 
    352     /**
    353      * Guarantees that the conversation has been created in the database.
    354      * This will make a blocking database call if it hasn't.
    355      *
    356      * @return The thread ID of this conversation in the database
    357      */
    358     public synchronized long ensureThreadId() {
    359         if (DEBUG) {
    360             LogTag.debug("ensureThreadId before: " + mThreadId);
    361         }
    362         if (mThreadId <= 0) {
    363             mThreadId = getOrCreateThreadId(mContext, mRecipients);
    364         }
    365         if (DEBUG) {
    366             LogTag.debug("ensureThreadId after: " + mThreadId);
    367         }
    368 
    369         return mThreadId;
    370     }
    371 
    372     public synchronized void clearThreadId() {
    373         // remove ourself from the cache
    374         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    375             LogTag.debug("clearThreadId old threadId was: " + mThreadId + " now zero");
    376         }
    377         Cache.remove(mThreadId);
    378 
    379         mThreadId = 0;
    380     }
    381 
    382     /**
    383      * Sets the list of recipients associated with this conversation.
    384      * If called, {@link #ensureThreadId} must be called before the next
    385      * operation that depends on this conversation existing in the
    386      * database (e.g. storing a draft message to it).
    387      */
    388     public synchronized void setRecipients(ContactList list) {
    389         mRecipients = list;
    390 
    391         // Invalidate thread ID because the recipient set has changed.
    392         mThreadId = 0;
    393     }
    394 
    395     /**
    396      * Returns the recipient set of this conversation.
    397      */
    398     public synchronized ContactList getRecipients() {
    399         return mRecipients;
    400     }
    401 
    402     /**
    403      * Returns true if a draft message exists in this conversation.
    404      */
    405     public synchronized boolean hasDraft() {
    406         if (mThreadId <= 0)
    407             return false;
    408 
    409         return DraftCache.getInstance().hasDraft(mThreadId);
    410     }
    411 
    412     /**
    413      * Sets whether or not this conversation has a draft message.
    414      */
    415     public synchronized void setDraftState(boolean hasDraft) {
    416         if (mThreadId <= 0)
    417             return;
    418 
    419         DraftCache.getInstance().setDraftState(mThreadId, hasDraft);
    420     }
    421 
    422     /**
    423      * Returns the time of the last update to this conversation in milliseconds,
    424      * on the {@link System#currentTimeMillis} timebase.
    425      */
    426     public synchronized long getDate() {
    427         return mDate;
    428     }
    429 
    430     /**
    431      * Returns the number of messages in this conversation, excluding the draft
    432      * (if it exists).
    433      */
    434     public synchronized int getMessageCount() {
    435         return mMessageCount;
    436     }
    437 
    438     /**
    439      * Returns a snippet of text from the most recent message in the conversation.
    440      */
    441     public synchronized String getSnippet() {
    442         return mSnippet;
    443     }
    444 
    445     /**
    446      * Returns true if there are any unread messages in the conversation.
    447      */
    448     public boolean hasUnreadMessages() {
    449         synchronized (this) {
    450             return mHasUnreadMessages;
    451         }
    452     }
    453 
    454     private void setHasUnreadMessages(boolean flag) {
    455         synchronized (this) {
    456             mHasUnreadMessages = flag;
    457         }
    458     }
    459 
    460     /**
    461      * Returns true if any messages in the conversation have attachments.
    462      */
    463     public synchronized boolean hasAttachment() {
    464         return mHasAttachment;
    465     }
    466 
    467     /**
    468      * Returns true if any messages in the conversation are in an error state.
    469      */
    470     public synchronized boolean hasError() {
    471         return mHasError;
    472     }
    473 
    474     private static long getOrCreateThreadId(Context context, ContactList list) {
    475         HashSet<String> recipients = new HashSet<String>();
    476         Contact cacheContact = null;
    477         for (Contact c : list) {
    478             cacheContact = Contact.get(c.getNumber(), false);
    479             if (cacheContact != null) {
    480                 recipients.add(cacheContact.getNumber());
    481             } else {
    482                 recipients.add(c.getNumber());
    483             }
    484         }
    485         long retVal = Threads.getOrCreateThreadId(context, recipients);
    486         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    487             LogTag.debug("[Conversation] getOrCreateThreadId for (%s) returned %d",
    488                     recipients, retVal);
    489         }
    490 
    491         return retVal;
    492     }
    493 
    494     /*
    495      * The primary key of a conversation is its recipient set; override
    496      * equals() and hashCode() to just pass through to the internal
    497      * recipient sets.
    498      */
    499     @Override
    500     public synchronized boolean equals(Object obj) {
    501         try {
    502             Conversation other = (Conversation)obj;
    503             return (mRecipients.equals(other.mRecipients));
    504         } catch (ClassCastException e) {
    505             return false;
    506         }
    507     }
    508 
    509     @Override
    510     public synchronized int hashCode() {
    511         return mRecipients.hashCode();
    512     }
    513 
    514     @Override
    515     public synchronized String toString() {
    516         return String.format("[%s] (tid %d)", mRecipients.serialize(), mThreadId);
    517     }
    518 
    519     /**
    520      * Remove any obsolete conversations sitting around on disk. Obsolete threads are threads
    521      * that aren't referenced by any message in the pdu or sms tables.
    522      */
    523     public static void asyncDeleteObsoleteThreads(AsyncQueryHandler handler, int token) {
    524         handler.startDelete(token, null, Threads.OBSOLETE_THREADS_URI, null, null);
    525     }
    526 
    527     /**
    528      * Start a query for all conversations in the database on the specified
    529      * AsyncQueryHandler.
    530      *
    531      * @param handler An AsyncQueryHandler that will receive onQueryComplete
    532      *                upon completion of the query
    533      * @param token   The token that will be passed to onQueryComplete
    534      */
    535     public static void startQueryForAll(AsyncQueryHandler handler, int token) {
    536         handler.cancelOperation(token);
    537 
    538         // This query looks like this in the log:
    539         // I/Database(  147): elapsedTime4Sql|/data/data/com.android.providers.telephony/databases/
    540         // mmssms.db|2.253 ms|SELECT _id, date, message_count, recipient_ids, snippet, snippet_cs,
    541         // read, error, has_attachment FROM threads ORDER BY  date DESC
    542 
    543         handler.startQuery(token, null, sAllThreadsUri,
    544                 ALL_THREADS_PROJECTION, null, null, Conversations.DEFAULT_SORT_ORDER);
    545     }
    546 
    547     /**
    548      * Start a delete of the conversation with the specified thread ID.
    549      *
    550      * @param handler An AsyncQueryHandler that will receive onDeleteComplete
    551      *                upon completion of the conversation being deleted
    552      * @param token   The token that will be passed to onDeleteComplete
    553      * @param deleteAll Delete the whole thread including locked messages
    554      * @param threadId Thread ID of the conversation to be deleted
    555      */
    556     public static void startDelete(AsyncQueryHandler handler, int token, boolean deleteAll,
    557             long threadId) {
    558         Uri uri = ContentUris.withAppendedId(Threads.CONTENT_URI, threadId);
    559         String selection = deleteAll ? null : "locked=0";
    560         handler.startDelete(token, null, uri, selection, null);
    561     }
    562 
    563     /**
    564      * Start deleting all conversations in the database.
    565      * @param handler An AsyncQueryHandler that will receive onDeleteComplete
    566      *                upon completion of all conversations being deleted
    567      * @param token   The token that will be passed to onDeleteComplete
    568      * @param deleteAll Delete the whole thread including locked messages
    569      */
    570     public static void startDeleteAll(AsyncQueryHandler handler, int token, boolean deleteAll) {
    571         String selection = deleteAll ? null : "locked=0";
    572         handler.startDelete(token, null, Threads.CONTENT_URI, selection, null);
    573     }
    574 
    575     /**
    576      * Check for locked messages in all threads or a specified thread.
    577      * @param handler An AsyncQueryHandler that will receive onQueryComplete
    578      *                upon completion of looking for locked messages
    579      * @param threadId   The threadId of the thread to search. -1 means all threads
    580      * @param token   The token that will be passed to onQueryComplete
    581      */
    582     public static void startQueryHaveLockedMessages(AsyncQueryHandler handler, long threadId,
    583             int token) {
    584         handler.cancelOperation(token);
    585         Uri uri = MmsSms.CONTENT_LOCKED_URI;
    586         if (threadId != -1) {
    587             uri = ContentUris.withAppendedId(uri, threadId);
    588         }
    589         handler.startQuery(token, new Long(threadId), uri,
    590                 ALL_THREADS_PROJECTION, null, null, Conversations.DEFAULT_SORT_ORDER);
    591     }
    592 
    593     /**
    594      * Fill the specified conversation with the values from the specified
    595      * cursor, possibly setting recipients to empty if {@value allowQuery}
    596      * is false and the recipient IDs are not in cache.  The cursor should
    597      * be one made via {@link #startQueryForAll}.
    598      */
    599     private static void fillFromCursor(Context context, Conversation conv,
    600                                        Cursor c, boolean allowQuery) {
    601         synchronized (conv) {
    602             conv.mThreadId = c.getLong(ID);
    603             conv.mDate = c.getLong(DATE);
    604             conv.mMessageCount = c.getInt(MESSAGE_COUNT);
    605 
    606             // Replace the snippet with a default value if it's empty.
    607             String snippet = MessageUtils.extractEncStrFromCursor(c, SNIPPET, SNIPPET_CS);
    608             if (TextUtils.isEmpty(snippet)) {
    609                 snippet = context.getString(R.string.no_subject_view);
    610             }
    611             conv.mSnippet = snippet;
    612 
    613             conv.setHasUnreadMessages(c.getInt(READ) == 0);
    614             conv.mHasError = (c.getInt(ERROR) != 0);
    615             conv.mHasAttachment = (c.getInt(HAS_ATTACHMENT) != 0);
    616         }
    617         // Fill in as much of the conversation as we can before doing the slow stuff of looking
    618         // up the contacts associated with this conversation.
    619         String recipientIds = c.getString(RECIPIENT_IDS);
    620         ContactList recipients = ContactList.getByIds(recipientIds, allowQuery);
    621         synchronized (conv) {
    622             conv.mRecipients = recipients;
    623         }
    624 
    625         if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) {
    626             LogTag.debug("fillFromCursor: conv=" + conv + ", recipientIds=" + recipientIds);
    627         }
    628     }
    629 
    630     /**
    631      * Private cache for the use of the various forms of Conversation.get.
    632      */
    633     private static class Cache {
    634         private static Cache sInstance = new Cache();
    635         static Cache getInstance() { return sInstance; }
    636         private final HashSet<Conversation> mCache;
    637         private Cache() {
    638             mCache = new HashSet<Conversation>(10);
    639         }
    640 
    641         /**
    642          * Return the conversation with the specified thread ID, or
    643          * null if it's not in cache.
    644          */
    645         static Conversation get(long threadId) {
    646             synchronized (sInstance) {
    647                 if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) {
    648                     LogTag.debug("Conversation get with threadId: " + threadId);
    649                 }
    650                 for (Conversation c : sInstance.mCache) {
    651                     if (DEBUG) {
    652                         LogTag.debug("Conversation get() threadId: " + threadId +
    653                                 " c.getThreadId(): " + c.getThreadId());
    654                     }
    655                     if (c.getThreadId() == threadId) {
    656                         return c;
    657                     }
    658                 }
    659             }
    660             return null;
    661         }
    662 
    663         /**
    664          * Return the conversation with the specified recipient
    665          * list, or null if it's not in cache.
    666          */
    667         static Conversation get(ContactList list) {
    668             synchronized (sInstance) {
    669                 if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) {
    670                     LogTag.debug("Conversation get with ContactList: " + list);
    671                 }
    672                 for (Conversation c : sInstance.mCache) {
    673                     if (c.getRecipients().equals(list)) {
    674                         return c;
    675                     }
    676                 }
    677             }
    678             return null;
    679         }
    680 
    681         /**
    682          * Put the specified conversation in the cache.  The caller
    683          * should not place an already-existing conversation in the
    684          * cache, but rather update it in place.
    685          */
    686         static void put(Conversation c) {
    687             synchronized (sInstance) {
    688                 // We update cache entries in place so people with long-
    689                 // held references get updated.
    690                 if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) {
    691                     LogTag.debug("Conversation.Cache.put: conv= " + c + ", hash: " + c.hashCode());
    692                 }
    693 
    694                 if (sInstance.mCache.contains(c)) {
    695                     throw new IllegalStateException("cache already contains " + c +
    696                             " threadId: " + c.mThreadId);
    697                 }
    698                 sInstance.mCache.add(c);
    699             }
    700         }
    701 
    702         static void remove(long threadId) {
    703             if (DEBUG) {
    704                 LogTag.debug("remove threadid: " + threadId);
    705                 dumpCache();
    706             }
    707             for (Conversation c : sInstance.mCache) {
    708                 if (c.getThreadId() == threadId) {
    709                     sInstance.mCache.remove(c);
    710                     return;
    711                 }
    712             }
    713         }
    714 
    715         static void dumpCache() {
    716             synchronized (sInstance) {
    717                 LogTag.debug("Conversation dumpCache: ");
    718                 for (Conversation c : sInstance.mCache) {
    719                     LogTag.debug("   conv: " + c.toString() + " hash: " + c.hashCode());
    720                 }
    721             }
    722         }
    723 
    724         /**
    725          * Remove all conversations from the cache that are not in
    726          * the provided set of thread IDs.
    727          */
    728         static void keepOnly(Set<Long> threads) {
    729             synchronized (sInstance) {
    730                 Iterator<Conversation> iter = sInstance.mCache.iterator();
    731                 while (iter.hasNext()) {
    732                     Conversation c = iter.next();
    733                     if (!threads.contains(c.getThreadId())) {
    734                         iter.remove();
    735                     }
    736                 }
    737             }
    738             if (DEBUG) {
    739                 LogTag.debug("after keepOnly");
    740                 dumpCache();
    741             }
    742         }
    743     }
    744 
    745     /**
    746      * Set up the conversation cache.  To be called once at application
    747      * startup time.
    748      */
    749     public static void init(final Context context) {
    750         new Thread(new Runnable() {
    751             public void run() {
    752                 cacheAllThreads(context);
    753             }
    754         }).start();
    755     }
    756 
    757     public static void markAllConversationsAsSeen(final Context context) {
    758         if (DEBUG) {
    759             LogTag.debug("Conversation.markAllConversationsAsSeen");
    760         }
    761 
    762         new Thread(new Runnable() {
    763             public void run() {
    764                 blockingMarkAllSmsMessagesAsSeen(context);
    765                 blockingMarkAllMmsMessagesAsSeen(context);
    766 
    767                 // Always update notifications regardless of the read state.
    768                 MessagingNotification.blockingUpdateAllNotifications(context);
    769             }
    770         }).start();
    771     }
    772 
    773     private static void blockingMarkAllSmsMessagesAsSeen(final Context context) {
    774         ContentResolver resolver = context.getContentResolver();
    775         Cursor cursor = resolver.query(Sms.Inbox.CONTENT_URI,
    776                 SEEN_PROJECTION,
    777                 "seen=0",
    778                 null,
    779                 null);
    780 
    781         int count = 0;
    782 
    783         if (cursor != null) {
    784             try {
    785                 count = cursor.getCount();
    786             } finally {
    787                 cursor.close();
    788             }
    789         }
    790 
    791         if (count == 0) {
    792             return;
    793         }
    794 
    795         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    796             Log.d(TAG, "mark " + count + " SMS msgs as seen");
    797         }
    798 
    799         ContentValues values = new ContentValues(1);
    800         values.put("seen", 1);
    801 
    802         resolver.update(Sms.Inbox.CONTENT_URI,
    803                 values,
    804                 "seen=0",
    805                 null);
    806     }
    807 
    808     private static void blockingMarkAllMmsMessagesAsSeen(final Context context) {
    809         ContentResolver resolver = context.getContentResolver();
    810         Cursor cursor = resolver.query(Mms.Inbox.CONTENT_URI,
    811                 SEEN_PROJECTION,
    812                 "seen=0",
    813                 null,
    814                 null);
    815 
    816         int count = 0;
    817 
    818         if (cursor != null) {
    819             try {
    820                 count = cursor.getCount();
    821             } finally {
    822                 cursor.close();
    823             }
    824         }
    825 
    826         if (count == 0) {
    827             return;
    828         }
    829 
    830         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    831             Log.d(TAG, "mark " + count + " MMS msgs as seen");
    832         }
    833 
    834         ContentValues values = new ContentValues(1);
    835         values.put("seen", 1);
    836 
    837         resolver.update(Mms.Inbox.CONTENT_URI,
    838                 values,
    839                 "seen=0",
    840                 null);
    841 
    842     }
    843 
    844     /**
    845      * Are we in the process of loading and caching all the threads?.
    846      */
    847     public static boolean loadingThreads() {
    848         synchronized (Cache.getInstance()) {
    849             return mLoadingThreads;
    850         }
    851     }
    852 
    853     private static void cacheAllThreads(Context context) {
    854         if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) {
    855             LogTag.debug("[Conversation] cacheAllThreads: begin");
    856         }
    857         synchronized (Cache.getInstance()) {
    858             if (mLoadingThreads) {
    859                 return;
    860                 }
    861             mLoadingThreads = true;
    862         }
    863 
    864         // Keep track of what threads are now on disk so we
    865         // can discard anything removed from the cache.
    866         HashSet<Long> threadsOnDisk = new HashSet<Long>();
    867 
    868         // Query for all conversations.
    869         Cursor c = context.getContentResolver().query(sAllThreadsUri,
    870                 ALL_THREADS_PROJECTION, null, null, null);
    871         try {
    872             if (c != null) {
    873                 while (c.moveToNext()) {
    874                     long threadId = c.getLong(ID);
    875                     threadsOnDisk.add(threadId);
    876 
    877                     // Try to find this thread ID in the cache.
    878                     Conversation conv;
    879                     synchronized (Cache.getInstance()) {
    880                         conv = Cache.get(threadId);
    881                     }
    882 
    883                     if (conv == null) {
    884                         // Make a new Conversation and put it in
    885                         // the cache if necessary.
    886                         conv = new Conversation(context, c, true);
    887                         try {
    888                             synchronized (Cache.getInstance()) {
    889                                 Cache.put(conv);
    890                             }
    891                         } catch (IllegalStateException e) {
    892                             LogTag.error("Tried to add duplicate Conversation to Cache");
    893                         }
    894                     } else {
    895                         // Or update in place so people with references
    896                         // to conversations get updated too.
    897                         fillFromCursor(context, conv, c, true);
    898                     }
    899                 }
    900             }
    901         } finally {
    902             if (c != null) {
    903                 c.close();
    904             }
    905             synchronized (Cache.getInstance()) {
    906                 mLoadingThreads = false;
    907             }
    908         }
    909 
    910         // Purge the cache of threads that no longer exist on disk.
    911         Cache.keepOnly(threadsOnDisk);
    912 
    913         if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) {
    914             LogTag.debug("[Conversation] cacheAllThreads: finished");
    915             Cache.dumpCache();
    916         }
    917     }
    918 
    919     private boolean loadFromThreadId(long threadId, boolean allowQuery) {
    920         Cursor c = mContext.getContentResolver().query(sAllThreadsUri, ALL_THREADS_PROJECTION,
    921                 "_id=" + Long.toString(threadId), null, null);
    922         try {
    923             if (c.moveToFirst()) {
    924                 fillFromCursor(mContext, this, c, allowQuery);
    925 
    926                 if (threadId != mThreadId) {
    927                     LogTag.error("loadFromThreadId: fillFromCursor returned differnt thread_id!" +
    928                             " threadId=" + threadId + ", mThreadId=" + mThreadId);
    929                 }
    930             } else {
    931                 LogTag.error("loadFromThreadId: Can't find thread ID " + threadId);
    932                 return false;
    933             }
    934         } finally {
    935             c.close();
    936         }
    937         return true;
    938     }
    939 
    940     private static String getRecipients(Uri uri) {
    941         String base = uri.getSchemeSpecificPart();
    942         int pos = base.indexOf('?');
    943         return (pos == -1) ? base : base.substring(0, pos);
    944     }
    945 }
    946