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