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