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