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