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