Conversation.java revision 2426db8887f7f1c0c93dbab5a10663cb22575ccd
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[] 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        startQuery(handler, token, null);
544    }
545
546    /**
547     * Start a query for in the database on the specified AsyncQueryHandler with the specified
548     * "where" clause.
549     *
550     * @param handler An AsyncQueryHandler that will receive onQueryComplete
551     *                upon completion of the query
552     * @param token   The token that will be passed to onQueryComplete
553     * @param selection   A where clause (can be null) to select particular conv items.
554     */
555    public static void startQuery(AsyncQueryHandler handler, int token, String selection) {
556        handler.cancelOperation(token);
557
558        // This query looks like this in the log:
559        // I/Database(  147): elapsedTime4Sql|/data/data/com.android.providers.telephony/databases/
560        // mmssms.db|2.253 ms|SELECT _id, date, message_count, recipient_ids, snippet, snippet_cs,
561        // read, error, has_attachment FROM threads ORDER BY  date DESC
562
563        handler.startQuery(token, null, sAllThreadsUri,
564                ALL_THREADS_PROJECTION, selection, null, Conversations.DEFAULT_SORT_ORDER);
565    }
566
567    /**
568     * Start a delete of the conversation with the specified thread ID.
569     *
570     * @param handler An AsyncQueryHandler that will receive onDeleteComplete
571     *                upon completion of the conversation being deleted
572     * @param token   The token that will be passed to onDeleteComplete
573     * @param deleteAll Delete the whole thread including locked messages
574     * @param threadId Thread ID of the conversation to be deleted
575     */
576    public static void startDelete(AsyncQueryHandler handler, int token, boolean deleteAll,
577            long threadId) {
578        Uri uri = ContentUris.withAppendedId(Threads.CONTENT_URI, threadId);
579        String selection = deleteAll ? null : "locked=0";
580        handler.startDelete(token, null, uri, selection, null);
581    }
582
583    /**
584     * Start deleting all conversations in the database.
585     * @param handler An AsyncQueryHandler that will receive onDeleteComplete
586     *                upon completion of all conversations being deleted
587     * @param token   The token that will be passed to onDeleteComplete
588     * @param deleteAll Delete the whole thread including locked messages
589     */
590    public static void startDeleteAll(AsyncQueryHandler handler, int token, boolean deleteAll) {
591        String selection = deleteAll ? null : "locked=0";
592        handler.startDelete(token, null, Threads.CONTENT_URI, selection, null);
593    }
594
595    /**
596     * Check for locked messages in all threads or a specified thread.
597     * @param handler An AsyncQueryHandler that will receive onQueryComplete
598     *                upon completion of looking for locked messages
599     * @param threadId   The threadId of the thread to search. -1 means all threads
600     * @param token   The token that will be passed to onQueryComplete
601     */
602    public static void startQueryHaveLockedMessages(AsyncQueryHandler handler, long threadId,
603            int token) {
604        handler.cancelOperation(token);
605        Uri uri = MmsSms.CONTENT_LOCKED_URI;
606        if (threadId != -1) {
607            uri = ContentUris.withAppendedId(uri, threadId);
608        }
609        handler.startQuery(token, new Long(threadId), uri,
610                ALL_THREADS_PROJECTION, null, null, Conversations.DEFAULT_SORT_ORDER);
611    }
612
613    /**
614     * Fill the specified conversation with the values from the specified
615     * cursor, possibly setting recipients to empty if {@value allowQuery}
616     * is false and the recipient IDs are not in cache.  The cursor should
617     * be one made via {@link #startQueryForAll}.
618     */
619    private static void fillFromCursor(Context context, Conversation conv,
620                                       Cursor c, boolean allowQuery) {
621        synchronized (conv) {
622            conv.mThreadId = c.getLong(ID);
623            conv.mDate = c.getLong(DATE);
624            conv.mMessageCount = c.getInt(MESSAGE_COUNT);
625
626            // Replace the snippet with a default value if it's empty.
627            String snippet = MessageUtils.extractEncStrFromCursor(c, SNIPPET, SNIPPET_CS);
628            if (TextUtils.isEmpty(snippet)) {
629                snippet = context.getString(R.string.no_subject_view);
630            }
631            conv.mSnippet = snippet;
632
633            conv.setHasUnreadMessages(c.getInt(READ) == 0);
634            conv.mHasError = (c.getInt(ERROR) != 0);
635            conv.mHasAttachment = (c.getInt(HAS_ATTACHMENT) != 0);
636        }
637        // Fill in as much of the conversation as we can before doing the slow stuff of looking
638        // up the contacts associated with this conversation.
639        String recipientIds = c.getString(RECIPIENT_IDS);
640        ContactList recipients = ContactList.getByIds(recipientIds, allowQuery);
641        synchronized (conv) {
642            conv.mRecipients = recipients;
643        }
644
645        if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) {
646            LogTag.debug("fillFromCursor: conv=" + conv + ", recipientIds=" + recipientIds);
647        }
648    }
649
650    /**
651     * Private cache for the use of the various forms of Conversation.get.
652     */
653    private static class Cache {
654        private static Cache sInstance = new Cache();
655        static Cache getInstance() { return sInstance; }
656        private final HashSet<Conversation> mCache;
657        private Cache() {
658            mCache = new HashSet<Conversation>(10);
659        }
660
661        /**
662         * Return the conversation with the specified thread ID, or
663         * null if it's not in cache.
664         */
665        static Conversation get(long threadId) {
666            synchronized (sInstance) {
667                if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) {
668                    LogTag.debug("Conversation get with threadId: " + threadId);
669                }
670                for (Conversation c : sInstance.mCache) {
671                    if (DEBUG) {
672                        LogTag.debug("Conversation get() threadId: " + threadId +
673                                " c.getThreadId(): " + c.getThreadId());
674                    }
675                    if (c.getThreadId() == threadId) {
676                        return c;
677                    }
678                }
679            }
680            return null;
681        }
682
683        /**
684         * Return the conversation with the specified recipient
685         * list, or null if it's not in cache.
686         */
687        static Conversation get(ContactList list) {
688            synchronized (sInstance) {
689                if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) {
690                    LogTag.debug("Conversation get with ContactList: " + list);
691                }
692                for (Conversation c : sInstance.mCache) {
693                    if (c.getRecipients().equals(list)) {
694                        return c;
695                    }
696                }
697            }
698            return null;
699        }
700
701        /**
702         * Put the specified conversation in the cache.  The caller
703         * should not place an already-existing conversation in the
704         * cache, but rather update it in place.
705         */
706        static void put(Conversation c) {
707            synchronized (sInstance) {
708                // We update cache entries in place so people with long-
709                // held references get updated.
710                if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) {
711                    LogTag.debug("Conversation.Cache.put: conv= " + c + ", hash: " + c.hashCode());
712                }
713
714                if (sInstance.mCache.contains(c)) {
715                    throw new IllegalStateException("cache already contains " + c +
716                            " threadId: " + c.mThreadId);
717                }
718                sInstance.mCache.add(c);
719            }
720        }
721
722        static void remove(long threadId) {
723            synchronized (sInstance) {
724                if (DEBUG) {
725                    LogTag.debug("remove threadid: " + threadId);
726                    dumpCache();
727                }
728                for (Conversation c : sInstance.mCache) {
729                    if (c.getThreadId() == threadId) {
730                        sInstance.mCache.remove(c);
731                        return;
732                    }
733                }
734            }
735        }
736
737        static void dumpCache() {
738            synchronized (sInstance) {
739                LogTag.debug("Conversation dumpCache: ");
740                for (Conversation c : sInstance.mCache) {
741                    LogTag.debug("   conv: " + c.toString() + " hash: " + c.hashCode());
742                }
743            }
744        }
745
746        /**
747         * Remove all conversations from the cache that are not in
748         * the provided set of thread IDs.
749         */
750        static void keepOnly(Set<Long> threads) {
751            synchronized (sInstance) {
752                Iterator<Conversation> iter = sInstance.mCache.iterator();
753                while (iter.hasNext()) {
754                    Conversation c = iter.next();
755                    if (!threads.contains(c.getThreadId())) {
756                        iter.remove();
757                    }
758                }
759            }
760            if (DEBUG) {
761                LogTag.debug("after keepOnly");
762                dumpCache();
763            }
764        }
765    }
766
767    /**
768     * Set up the conversation cache.  To be called once at application
769     * startup time.
770     */
771    public static void init(final Context context) {
772        new Thread(new Runnable() {
773            public void run() {
774                cacheAllThreads(context);
775            }
776        }).start();
777    }
778
779    public static void markAllConversationsAsSeen(final Context context) {
780        if (DEBUG) {
781            LogTag.debug("Conversation.markAllConversationsAsSeen");
782        }
783
784        new Thread(new Runnable() {
785            public void run() {
786                blockingMarkAllSmsMessagesAsSeen(context);
787                blockingMarkAllMmsMessagesAsSeen(context);
788
789                // Always update notifications regardless of the read state.
790                MessagingNotification.blockingUpdateAllNotifications(context);
791            }
792        }).start();
793    }
794
795    private static void blockingMarkAllSmsMessagesAsSeen(final Context context) {
796        ContentResolver resolver = context.getContentResolver();
797        Cursor cursor = resolver.query(Sms.Inbox.CONTENT_URI,
798                SEEN_PROJECTION,
799                "seen=0",
800                null,
801                null);
802
803        int count = 0;
804
805        if (cursor != null) {
806            try {
807                count = cursor.getCount();
808            } finally {
809                cursor.close();
810            }
811        }
812
813        if (count == 0) {
814            return;
815        }
816
817        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
818            Log.d(TAG, "mark " + count + " SMS msgs as seen");
819        }
820
821        ContentValues values = new ContentValues(1);
822        values.put("seen", 1);
823
824        resolver.update(Sms.Inbox.CONTENT_URI,
825                values,
826                "seen=0",
827                null);
828    }
829
830    private static void blockingMarkAllMmsMessagesAsSeen(final Context context) {
831        ContentResolver resolver = context.getContentResolver();
832        Cursor cursor = resolver.query(Mms.Inbox.CONTENT_URI,
833                SEEN_PROJECTION,
834                "seen=0",
835                null,
836                null);
837
838        int count = 0;
839
840        if (cursor != null) {
841            try {
842                count = cursor.getCount();
843            } finally {
844                cursor.close();
845            }
846        }
847
848        if (count == 0) {
849            return;
850        }
851
852        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
853            Log.d(TAG, "mark " + count + " MMS msgs as seen");
854        }
855
856        ContentValues values = new ContentValues(1);
857        values.put("seen", 1);
858
859        resolver.update(Mms.Inbox.CONTENT_URI,
860                values,
861                "seen=0",
862                null);
863
864    }
865
866    /**
867     * Are we in the process of loading and caching all the threads?.
868     */
869    public static boolean loadingThreads() {
870        synchronized (Cache.getInstance()) {
871            return mLoadingThreads;
872        }
873    }
874
875    private static void cacheAllThreads(Context context) {
876        if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) {
877            LogTag.debug("[Conversation] cacheAllThreads: begin");
878        }
879        synchronized (Cache.getInstance()) {
880            if (mLoadingThreads) {
881                return;
882                }
883            mLoadingThreads = true;
884        }
885
886        // Keep track of what threads are now on disk so we
887        // can discard anything removed from the cache.
888        HashSet<Long> threadsOnDisk = new HashSet<Long>();
889
890        // Query for all conversations.
891        Cursor c = context.getContentResolver().query(sAllThreadsUri,
892                ALL_THREADS_PROJECTION, null, null, null);
893        try {
894            if (c != null) {
895                while (c.moveToNext()) {
896                    long threadId = c.getLong(ID);
897                    threadsOnDisk.add(threadId);
898
899                    // Try to find this thread ID in the cache.
900                    Conversation conv;
901                    synchronized (Cache.getInstance()) {
902                        conv = Cache.get(threadId);
903                    }
904
905                    if (conv == null) {
906                        // Make a new Conversation and put it in
907                        // the cache if necessary.
908                        conv = new Conversation(context, c, true);
909                        try {
910                            synchronized (Cache.getInstance()) {
911                                Cache.put(conv);
912                            }
913                        } catch (IllegalStateException e) {
914                            LogTag.error("Tried to add duplicate Conversation to Cache");
915                        }
916                    } else {
917                        // Or update in place so people with references
918                        // to conversations get updated too.
919                        fillFromCursor(context, conv, c, true);
920                    }
921                }
922            }
923        } finally {
924            if (c != null) {
925                c.close();
926            }
927            synchronized (Cache.getInstance()) {
928                mLoadingThreads = false;
929            }
930        }
931
932        // Purge the cache of threads that no longer exist on disk.
933        Cache.keepOnly(threadsOnDisk);
934
935        if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) {
936            LogTag.debug("[Conversation] cacheAllThreads: finished");
937            Cache.dumpCache();
938        }
939    }
940
941    private boolean loadFromThreadId(long threadId, boolean allowQuery) {
942        Cursor c = mContext.getContentResolver().query(sAllThreadsUri, ALL_THREADS_PROJECTION,
943                "_id=" + Long.toString(threadId), null, null);
944        try {
945            if (c.moveToFirst()) {
946                fillFromCursor(mContext, this, c, allowQuery);
947
948                if (threadId != mThreadId) {
949                    LogTag.error("loadFromThreadId: fillFromCursor returned differnt thread_id!" +
950                            " threadId=" + threadId + ", mThreadId=" + mThreadId);
951                }
952            } else {
953                LogTag.error("loadFromThreadId: Can't find thread ID " + threadId);
954                return false;
955            }
956        } finally {
957            c.close();
958        }
959        return true;
960    }
961}
962