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