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