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