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