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