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