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