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