OSDN Git Service

original
[gb-231r1-is01/Gingerbread_2.3.3_r1_IS01.git] / packages / apps / Mms / src / com / android / mms / data / Conversation.java
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 = uri.getSchemeSpecificPart();
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 = uri.getSchemeSpecificPart();
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 }