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