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