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.database.sqlite.SqliteWrapper;
17 import android.net.Uri;
18 import android.os.AsyncTask;
19 import android.provider.BaseColumns;
20 import android.provider.Telephony.Mms;
21 import android.provider.Telephony.MmsSms;
22 import android.provider.Telephony.Sms;
23 import android.provider.Telephony.Sms.Conversations;
24 import android.provider.Telephony.Threads;
25 import android.provider.Telephony.ThreadsColumns;
26 import android.telephony.PhoneNumberUtils;
27 import android.text.TextUtils;
28 import android.util.Log;
29 
30 import com.android.mms.LogTag;
31 import com.android.mms.MmsApp;
32 import com.android.mms.R;
33 import com.android.mms.transaction.MessagingNotification;
34 import com.android.mms.transaction.MmsMessageSender;
35 import com.android.mms.ui.ComposeMessageActivity;
36 import com.android.mms.ui.MessageUtils;
37 import com.android.mms.util.AddressUtils;
38 import com.android.mms.util.DraftCache;
39 
40 import com.google.android.mms.pdu.PduHeaders;
41 
42 /**
43  * An interface for finding information about conversations and/or creating new ones.
44  */
45 public 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 
Conversation(Context context)105     private Conversation(Context context) {
106         mContext = context;
107         mRecipients = new ContactList();
108         mThreadId = 0;
109     }
110 
Conversation(Context context, long threadId, boolean allowQuery)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 
Conversation(Context context, Cursor cursor, boolean allowQuery)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      */
createNew(Context context)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      */
get(Context context, long threadId, boolean allowQuery)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      */
get(Context context, ContactList recipients, boolean allowQuery)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      */
get(Context context, Uri uri, boolean allowQuery)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      */
sameRecipient(Uri uri, Context context)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      */
from(Context context, Cursor cursor)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 
buildReadContentValues()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 
sendReadReport(final Context context, final long threadId, final int status)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      */
markAsRead()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      */
blockMarkAsRead(boolean block)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      */
getUri()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      */
getUri(long threadId)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      */
getThreadId()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      */
ensureThreadId()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 
clearThreadId()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      */
setRecipients(ContactList list)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      */
getRecipients()513     public synchronized ContactList getRecipients() {
514         return mRecipients;
515     }
516 
517     /**
518      * Returns true if a draft message exists in this conversation.
519      */
hasDraft()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      */
setDraftState(boolean hasDraft)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      */
getDate()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      */
getMessageCount()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      */
setMessageCount(int cnt)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      */
getSnippet()563     public synchronized String getSnippet() {
564         return mSnippet;
565     }
566 
567     /**
568      * Returns true if there are any unread messages in the conversation.
569      */
hasUnreadMessages()570     public boolean hasUnreadMessages() {
571         synchronized (this) {
572             return mHasUnreadMessages;
573         }
574     }
575 
setHasUnreadMessages(boolean flag)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      */
hasAttachment()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      */
hasError()592     public synchronized boolean hasError() {
593         return mHasError;
594     }
595 
596     /**
597      * Returns true if this conversation is selected for a multi-operation.
598      */
isChecked()599     public synchronized boolean isChecked() {
600         return mIsChecked;
601     }
602 
setIsChecked(boolean isChecked)603     public synchronized void setIsChecked(boolean isChecked) {
604         mIsChecked = isChecked;
605     }
606 
getOrCreateThreadId(Context context, ContactList list)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 
getOrCreateThreadId(Context context, String address)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
equals(Object obj)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
hashCode()693     public synchronized int hashCode() {
694         return mRecipients.hashCode();
695     }
696 
697     @Override
toString()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      */
asyncDeleteObsoleteThreads(AsyncQueryHandler handler, int token)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      */
startQueryForAll(AsyncQueryHandler handler, int token)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      */
startQuery(AsyncQueryHandler handler, int token, String selection)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      */
startDelete(ConversationQueryHandler handler, int token, boolean deleteAll, Collection<Long> threadIds)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      */
startDeleteAll(ConversationQueryHandler handler, int token, boolean deleteAll)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 
ConversationQueryHandler(ContentResolver cr)816         public ConversationQueryHandler(ContentResolver cr) {
817             super(cr);
818         }
819 
setDeleteToken(int token)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
onDeleteComplete(int token, Object cookie, int result)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      */
startQueryHaveLockedMessages(AsyncQueryHandler handler, Collection<Long> threadIds, int token)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      */
startQueryHaveLockedMessages(AsyncQueryHandler handler, long threadId, int token)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      */
fillFromCursor(Context context, Conversation conv, Cursor c, boolean allowQuery)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();
getInstance()942         static Cache getInstance() { return sInstance; }
943         private final HashSet<Conversation> mCache;
Cache()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          */
get(long threadId)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          */
get(ContactList list)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          */
put(Conversation c)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          */
replace(Conversation c)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 
remove(long threadId)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 
dumpCache()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          */
keepOnly(Set<Long> threads)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      */
init(final Context context)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 
markAllConversationsAsSeen(final Context context)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 
blockingMarkAllSmsMessagesAsSeen(final Context context)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 
blockingMarkAllMmsMessagesAsSeen(final Context context)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      */
loadingThreads()1201     public static boolean loadingThreads() {
1202         synchronized (Cache.getInstance()) {
1203             return sLoadingThreads;
1204         }
1205     }
1206 
cacheAllThreads(Context context)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 
loadFromThreadId(long threadId, boolean allowQuery)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 
getRecipients(Uri uri)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 
dump()1304     public static void dump() {
1305         Cache.dumpCache();
1306     }
1307 
dumpThreadsTable(Context context)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 
dumpSmsTable(Context context)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      */
verifySingleRecipient(final Context context, final long threadId, final String recipientStr)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