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