• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.mms.data;
2 
3 import java.util.HashSet;
4 import java.util.Iterator;
5 import java.util.Set;
6 
7 import android.content.AsyncQueryHandler;
8 import android.content.ContentUris;
9 import android.content.ContentValues;
10 import android.content.Context;
11 import android.database.Cursor;
12 import android.net.Uri;
13 import android.provider.Telephony.MmsSms;
14 import android.provider.Telephony.Threads;
15 import android.provider.Telephony.Sms.Conversations;
16 import android.text.TextUtils;
17 import android.util.Log;
18 
19 import com.android.mms.R;
20 import com.android.mms.LogTag;
21 import com.android.mms.transaction.MessagingNotification;
22 import com.android.mms.ui.MessageUtils;
23 import com.android.mms.util.DraftCache;
24 
25 /**
26  * An interface for finding information about conversations and/or creating new ones.
27  */
28 public class Conversation {
29     private static final String TAG = "Mms/conv";
30     private static final boolean DEBUG = false;
31 
32     private static final Uri sAllThreadsUri =
33         Threads.CONTENT_URI.buildUpon().appendQueryParameter("simple", "true").build();
34 
35     private static final String[] ALL_THREADS_PROJECTION = {
36         Threads._ID, Threads.DATE, Threads.MESSAGE_COUNT, Threads.RECIPIENT_IDS,
37         Threads.SNIPPET, Threads.SNIPPET_CHARSET, Threads.READ, Threads.ERROR,
38         Threads.HAS_ATTACHMENT
39     };
40     private static final int ID             = 0;
41     private static final int DATE           = 1;
42     private static final int MESSAGE_COUNT  = 2;
43     private static final int RECIPIENT_IDS  = 3;
44     private static final int SNIPPET        = 4;
45     private static final int SNIPPET_CS     = 5;
46     private static final int READ           = 6;
47     private static final int ERROR          = 7;
48     private static final int HAS_ATTACHMENT = 8;
49 
50 
51     private final Context mContext;
52 
53     // The thread ID of this conversation.  Can be zero in the case of a
54     // new conversation where the recipient set is changing as the user
55     // types and we have not hit the database yet to create a thread.
56     private long mThreadId;
57 
58     private ContactList mRecipients;    // The current set of recipients.
59     private long mDate;                 // The last update time.
60     private int mMessageCount;          // Number of messages.
61     private String mSnippet;            // Text of the most recent message.
62     private boolean mHasUnreadMessages; // True if there are unread messages.
63     private boolean mHasAttachment;     // True if any message has an attachment.
64     private boolean mHasError;          // True if any message is in an error state.
65 
66     private static ContentValues mReadContentValues;
67     private static boolean mLoadingThreads;
68 
69 
Conversation(Context context)70     private Conversation(Context context) {
71         mContext = context;
72         mRecipients = new ContactList();
73         mThreadId = 0;
74     }
75 
Conversation(Context context, long threadId)76     private Conversation(Context context, long threadId) {
77         mContext = context;
78         if (!loadFromThreadId(threadId)) {
79             mRecipients = new ContactList();
80             mThreadId = 0;
81         }
82     }
83 
Conversation(Context context, Cursor cursor, boolean allowQuery)84     private Conversation(Context context, Cursor cursor, boolean allowQuery) {
85         mContext = context;
86         fillFromCursor(context, this, cursor, allowQuery);
87     }
88 
89     /**
90      * Create a new conversation with no recipients.  {@link setRecipients} can
91      * be called as many times as you like; the conversation will not be
92      * created in the database until {@link ensureThreadId} is called.
93      */
createNew(Context context)94     public static Conversation createNew(Context context) {
95         return new Conversation(context);
96     }
97 
98     /**
99      * Find the conversation matching the provided thread ID.
100      */
get(Context context, long threadId)101     public static Conversation get(Context context, long threadId) {
102         synchronized (Cache.getInstance()) {
103             Conversation conv = Cache.get(threadId);
104             if (conv != null)
105                 return conv;
106 
107             conv = new Conversation(context, threadId);
108             try {
109                 Cache.put(conv);
110             } catch (IllegalStateException e) {
111                 LogTag.error("Tried to add duplicate Conversation to Cache");
112             }
113             return conv;
114         }
115     }
116 
117     /**
118      * Find the conversation matching the provided recipient set.
119      * When called with an empty recipient list, equivalent to {@link createEmpty}.
120      */
get(Context context, ContactList recipients)121     public static Conversation get(Context context, ContactList recipients) {
122         // If there are no recipients in the list, make a new conversation.
123         if (recipients.size() < 1) {
124             return createNew(context);
125         }
126 
127         synchronized (Cache.getInstance()) {
128             Conversation conv = Cache.get(recipients);
129             if (conv != null)
130                 return conv;
131 
132             long threadId = getOrCreateThreadId(context, recipients);
133             conv = new Conversation(context, threadId);
134             conv.setRecipients(recipients);
135 
136             try {
137                 Cache.put(conv);
138             } catch (IllegalStateException e) {
139                 LogTag.error("Tried to add duplicate Conversation to Cache");
140             }
141 
142             return conv;
143         }
144     }
145 
146     /**
147      * Find the conversation matching in the specified Uri.  Example
148      * forms: {@value content://mms-sms/conversations/3} or
149      * {@value sms:+12124797990}.
150      * When called with a null Uri, equivalent to {@link createEmpty}.
151      */
get(Context context, Uri uri)152     public static Conversation get(Context context, Uri uri) {
153         if (uri == null) {
154             return createNew(context);
155         }
156 
157         if (DEBUG) {
158             Log.v(TAG, "Conversation get URI: " + uri);
159         }
160         // Handle a conversation URI
161         if (uri.getPathSegments().size() >= 2) {
162             try {
163                 long threadId = Long.parseLong(uri.getPathSegments().get(1));
164                 if (DEBUG) {
165                     Log.v(TAG, "Conversation get threadId: " + threadId);
166                 }
167                 return get(context, threadId);
168             } catch (NumberFormatException exception) {
169                 LogTag.error("Invalid URI: " + uri);
170             }
171         }
172 
173         String recipient = uri.getSchemeSpecificPart();
174         return get(context, ContactList.getByNumbers(recipient,
175                 false /* don't block */, true /* replace number */));
176     }
177 
178     /**
179      * Returns true if the recipient in the uri matches the recipient list in this
180      * conversation.
181      */
sameRecipient(Uri uri)182     public boolean sameRecipient(Uri uri) {
183         int size = mRecipients.size();
184         if (size > 1) {
185             return false;
186         }
187         if (uri == null) {
188             return size == 0;
189         }
190         if (uri.getPathSegments().size() >= 2) {
191             return false;       // it's a thread id for a conversation
192         }
193         String recipient = uri.getSchemeSpecificPart();
194         ContactList incomingRecipient = ContactList.getByNumbers(recipient,
195                 false /* don't block */, false /* don't replace number */);
196         return mRecipients.equals(incomingRecipient);
197     }
198 
199     /**
200      * Returns a temporary Conversation (not representing one on disk) wrapping
201      * the contents of the provided cursor.  The cursor should be the one
202      * returned to your AsyncQueryHandler passed in to {@link startQueryForAll}.
203      * The recipient list of this conversation can be empty if the results
204      * were not in cache.
205      */
206     // TODO: check why can't load a cached Conversation object here.
from(Context context, Cursor cursor)207     public static Conversation from(Context context, Cursor cursor) {
208         return new Conversation(context, cursor, false);
209     }
210 
buildReadContentValues()211     private void buildReadContentValues() {
212         if (mReadContentValues == null) {
213             mReadContentValues = new ContentValues(1);
214             mReadContentValues.put("read", 1);
215         }
216     }
217 
218     /**
219      * Marks all messages in this conversation as read and updates
220      * relevant notifications.  This method returns immediately;
221      * work is dispatched to a background thread.
222      */
markAsRead()223     public synchronized void markAsRead() {
224         // If we have no Uri to mark (as in the case of a conversation that
225         // has not yet made its way to disk), there's nothing to do.
226         final Uri threadUri = getUri();
227 
228         new Thread(new Runnable() {
229             public void run() {
230                 if (threadUri != null) {
231                     buildReadContentValues();
232                     mContext.getContentResolver().update(threadUri, mReadContentValues,
233                             "read=0", null);
234                     mHasUnreadMessages = false;
235                 }
236                 // Always update notifications regardless of the read state.
237                 MessagingNotification.updateAllNotifications(mContext);
238             }
239         }).start();
240     }
241 
242     /**
243      * Returns a content:// URI referring to this conversation,
244      * or null if it does not exist on disk yet.
245      */
getUri()246     public synchronized Uri getUri() {
247         if (mThreadId <= 0)
248             return null;
249 
250         return ContentUris.withAppendedId(Threads.CONTENT_URI, mThreadId);
251     }
252 
253     /**
254      * Return the Uri for all messages in the given thread ID.
255      * @deprecated
256      */
getUri(long threadId)257     public static Uri getUri(long threadId) {
258         // TODO: Callers using this should really just have a Conversation
259         // and call getUri() on it, but this guarantees no blocking.
260         return ContentUris.withAppendedId(Threads.CONTENT_URI, threadId);
261     }
262 
263     /**
264      * Returns the thread ID of this conversation.  Can be zero if
265      * {@link ensureThreadId} has not been called yet.
266      */
getThreadId()267     public synchronized long getThreadId() {
268         return mThreadId;
269     }
270 
271     /**
272      * Guarantees that the conversation has been created in the database.
273      * This will make a blocking database call if it hasn't.
274      *
275      * @return The thread ID of this conversation in the database
276      */
ensureThreadId()277     public synchronized long ensureThreadId() {
278         if (DEBUG) {
279             LogTag.debug("ensureThreadId before: " + mThreadId);
280         }
281         if (mThreadId <= 0) {
282             mThreadId = getOrCreateThreadId(mContext, mRecipients);
283         }
284         if (DEBUG) {
285             LogTag.debug("ensureThreadId after: " + mThreadId);
286         }
287 
288         return mThreadId;
289     }
290 
clearThreadId()291     public synchronized void clearThreadId() {
292         // remove ourself from the cache
293         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
294             LogTag.debug("clearThreadId old threadId was: " + mThreadId + " now zero");
295         }
296         Cache.remove(mThreadId);
297 
298         mThreadId = 0;
299     }
300 
301     /**
302      * Sets the list of recipients associated with this conversation.
303      * If called, {@link ensureThreadId} must be called before the next
304      * operation that depends on this conversation existing in the
305      * database (e.g. storing a draft message to it).
306      */
setRecipients(ContactList list)307     public synchronized void setRecipients(ContactList list) {
308         mRecipients = list;
309 
310         // Invalidate thread ID because the recipient set has changed.
311         mThreadId = 0;
312     }
313 
314     /**
315      * Returns the recipient set of this conversation.
316      */
getRecipients()317     public synchronized ContactList getRecipients() {
318         return mRecipients;
319     }
320 
321     /**
322      * Returns true if a draft message exists in this conversation.
323      */
hasDraft()324     public synchronized boolean hasDraft() {
325         if (mThreadId <= 0)
326             return false;
327 
328         return DraftCache.getInstance().hasDraft(mThreadId);
329     }
330 
331     /**
332      * Sets whether or not this conversation has a draft message.
333      */
setDraftState(boolean hasDraft)334     public synchronized void setDraftState(boolean hasDraft) {
335         if (mThreadId <= 0)
336             return;
337 
338         DraftCache.getInstance().setDraftState(mThreadId, hasDraft);
339     }
340 
341     /**
342      * Returns the time of the last update to this conversation in milliseconds,
343      * on the {@link System.currentTimeMillis} timebase.
344      */
getDate()345     public synchronized long getDate() {
346         return mDate;
347     }
348 
349     /**
350      * Returns the number of messages in this conversation, excluding the draft
351      * (if it exists).
352      */
getMessageCount()353     public synchronized int getMessageCount() {
354         return mMessageCount;
355     }
356 
357     /**
358      * Returns a snippet of text from the most recent message in the conversation.
359      */
getSnippet()360     public synchronized String getSnippet() {
361         return mSnippet;
362     }
363 
364     /**
365      * Returns true if there are any unread messages in the conversation.
366      */
hasUnreadMessages()367     public synchronized boolean hasUnreadMessages() {
368         return mHasUnreadMessages;
369     }
370 
371     /**
372      * Returns true if any messages in the conversation have attachments.
373      */
hasAttachment()374     public synchronized boolean hasAttachment() {
375         return mHasAttachment;
376     }
377 
378     /**
379      * Returns true if any messages in the conversation are in an error state.
380      */
hasError()381     public synchronized boolean hasError() {
382         return mHasError;
383     }
384 
getOrCreateThreadId(Context context, ContactList list)385     private static long getOrCreateThreadId(Context context, ContactList list) {
386         HashSet<String> recipients = new HashSet<String>();
387         Contact cacheContact = null;
388         for (Contact c : list) {
389             cacheContact = Contact.get(c.getNumber(),true);
390             if (cacheContact != null) {
391                 recipients.add(cacheContact.getNumber());
392             } else {
393                 recipients.add(c.getNumber());
394             }
395         }
396         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
397             LogTag.debug("getOrCreateThreadId %s", recipients);
398         }
399         return Threads.getOrCreateThreadId(context, recipients);
400     }
401 
402     /*
403      * The primary key of a conversation is its recipient set; override
404      * equals() and hashCode() to just pass through to the internal
405      * recipient sets.
406      */
407     @Override
equals(Object obj)408     public synchronized boolean equals(Object obj) {
409         try {
410             Conversation other = (Conversation)obj;
411             return (mRecipients.equals(other.mRecipients));
412         } catch (ClassCastException e) {
413             return false;
414         }
415     }
416 
417     @Override
hashCode()418     public synchronized int hashCode() {
419         return mRecipients.hashCode();
420     }
421 
422     @Override
toString()423     public synchronized String toString() {
424         return String.format("[%s] (tid %d)", mRecipients.serialize(), mThreadId);
425     }
426 
427     /**
428      * Remove any obsolete conversations sitting around on disk.
429      * @deprecated
430      */
cleanup(Context context)431     public static void cleanup(Context context) {
432         // TODO: Get rid of this awful hack.
433         context.getContentResolver().delete(Threads.OBSOLETE_THREADS_URI, null, null);
434     }
435 
436     /**
437      * Start a query for all conversations in the database on the specified
438      * AsyncQueryHandler.
439      *
440      * @param handler An AsyncQueryHandler that will receive onQueryComplete
441      *                upon completion of the query
442      * @param token   The token that will be passed to onQueryComplete
443      */
startQueryForAll(AsyncQueryHandler handler, int token)444     public static void startQueryForAll(AsyncQueryHandler handler, int token) {
445         handler.cancelOperation(token);
446         handler.startQuery(token, null, sAllThreadsUri,
447                 ALL_THREADS_PROJECTION, null, null, Conversations.DEFAULT_SORT_ORDER);
448     }
449 
450     /**
451      * Start a delete of the conversation with the specified thread ID.
452      *
453      * @param handler An AsyncQueryHandler that will receive onDeleteComplete
454      *                upon completion of the conversation being deleted
455      * @param token   The token that will be passed to onDeleteComplete
456      * @param deleteAll Delete the whole thread including locked messages
457      * @param threadId Thread ID of the conversation to be deleted
458      */
startDelete(AsyncQueryHandler handler, int token, boolean deleteAll, long threadId)459     public static void startDelete(AsyncQueryHandler handler, int token, boolean deleteAll,
460             long threadId) {
461         Uri uri = ContentUris.withAppendedId(Threads.CONTENT_URI, threadId);
462         String selection = deleteAll ? null : "locked=0";
463         handler.startDelete(token, null, uri, selection, null);
464     }
465 
466     /**
467      * Start deleting all conversations in the database.
468      * @param handler An AsyncQueryHandler that will receive onDeleteComplete
469      *                upon completion of all conversations being deleted
470      * @param token   The token that will be passed to onDeleteComplete
471      * @param deleteAll Delete the whole thread including locked messages
472      */
startDeleteAll(AsyncQueryHandler handler, int token, boolean deleteAll)473     public static void startDeleteAll(AsyncQueryHandler handler, int token, boolean deleteAll) {
474         String selection = deleteAll ? null : "locked=0";
475         handler.startDelete(token, null, Threads.CONTENT_URI, selection, null);
476     }
477 
478     /**
479      * Check for locked messages in all threads or a specified thread.
480      * @param handler An AsyncQueryHandler that will receive onQueryComplete
481      *                upon completion of looking for locked messages
482      * @param threadId   The threadId of the thread to search. -1 means all threads
483      * @param token   The token that will be passed to onQueryComplete
484      */
startQueryHaveLockedMessages(AsyncQueryHandler handler, long threadId, int token)485     public static void startQueryHaveLockedMessages(AsyncQueryHandler handler, long threadId,
486             int token) {
487         handler.cancelOperation(token);
488         Uri uri = MmsSms.CONTENT_LOCKED_URI;
489         if (threadId != -1) {
490             uri = ContentUris.withAppendedId(uri, threadId);
491         }
492         handler.startQuery(token, new Long(threadId), uri,
493                 ALL_THREADS_PROJECTION, null, null, Conversations.DEFAULT_SORT_ORDER);
494     }
495 
496     /**
497      * Fill the specified conversation with the values from the specified
498      * cursor, possibly setting recipients to empty if {@value allowQuery}
499      * is false and the recipient IDs are not in cache.  The cursor should
500      * be one made via {@link startQueryForAll}.
501      */
fillFromCursor(Context context, Conversation conv, Cursor c, boolean allowQuery)502     private static void fillFromCursor(Context context, Conversation conv,
503                                        Cursor c, boolean allowQuery) {
504         synchronized (conv) {
505             conv.mThreadId = c.getLong(ID);
506             conv.mDate = c.getLong(DATE);
507             conv.mMessageCount = c.getInt(MESSAGE_COUNT);
508 
509             // Replace the snippet with a default value if it's empty.
510             String snippet = MessageUtils.extractEncStrFromCursor(c, SNIPPET, SNIPPET_CS);
511             if (TextUtils.isEmpty(snippet)) {
512                 snippet = context.getString(R.string.no_subject_view);
513             }
514             conv.mSnippet = snippet;
515 
516             conv.mHasUnreadMessages = (c.getInt(READ) == 0);
517             conv.mHasError = (c.getInt(ERROR) != 0);
518             conv.mHasAttachment = (c.getInt(HAS_ATTACHMENT) != 0);
519         }
520         // Fill in as much of the conversation as we can before doing the slow stuff of looking
521         // up the contacts associated with this conversation.
522         String recipientIds = c.getString(RECIPIENT_IDS);
523         ContactList recipients = ContactList.getByIds(recipientIds, allowQuery);;
524         synchronized (conv) {
525             conv.mRecipients = recipients;
526         }
527     }
528 
529     /**
530      * Private cache for the use of the various forms of Conversation.get.
531      */
532     private static class Cache {
533         private static Cache sInstance = new Cache();
getInstance()534         static Cache getInstance() { return sInstance; }
535         private final HashSet<Conversation> mCache;
Cache()536         private Cache() {
537             mCache = new HashSet<Conversation>(10);
538         }
539 
540         /**
541          * Return the conversation with the specified thread ID, or
542          * null if it's not in cache.
543          */
get(long threadId)544         static Conversation get(long threadId) {
545             synchronized (sInstance) {
546                 if (DEBUG) {
547                     LogTag.debug("Conversation get with threadId: " + threadId);
548                 }
549                 dumpCache();
550                 for (Conversation c : sInstance.mCache) {
551                     if (DEBUG) {
552                         LogTag.debug("Conversation get() threadId: " + threadId +
553                                 " c.getThreadId(): " + c.getThreadId());
554                     }
555                     if (c.getThreadId() == threadId) {
556                         return c;
557                     }
558                 }
559             }
560             return null;
561         }
562 
563         /**
564          * Return the conversation with the specified recipient
565          * list, or null if it's not in cache.
566          */
get(ContactList list)567         static Conversation get(ContactList list) {
568             synchronized (sInstance) {
569                 if (DEBUG) {
570                     LogTag.debug("Conversation get with ContactList: " + list);
571                     dumpCache();
572                 }
573                 for (Conversation c : sInstance.mCache) {
574                     if (c.getRecipients().equals(list)) {
575                         return c;
576                     }
577                 }
578             }
579             return null;
580         }
581 
582         /**
583          * Put the specified conversation in the cache.  The caller
584          * should not place an already-existing conversation in the
585          * cache, but rather update it in place.
586          */
put(Conversation c)587         static void put(Conversation c) {
588             synchronized (sInstance) {
589                 // We update cache entries in place so people with long-
590                 // held references get updated.
591                 if (DEBUG) {
592                     LogTag.debug("Conversation c: " + c + " put with threadid: " + c.getThreadId() +
593                             " c.hash: " + c.hashCode());
594                     dumpCache();
595                 }
596 
597                 if (sInstance.mCache.contains(c)) {
598                     throw new IllegalStateException("cache already contains " + c +
599                             " threadId: " + c.mThreadId);
600                 }
601                 sInstance.mCache.add(c);
602             }
603         }
604 
remove(long threadId)605         static void remove(long threadId) {
606             if (DEBUG) {
607                 LogTag.debug("remove threadid: " + threadId);
608                 dumpCache();
609             }
610             for (Conversation c : sInstance.mCache) {
611                 if (c.getThreadId() == threadId) {
612                     sInstance.mCache.remove(c);
613                     return;
614                 }
615             }
616         }
617 
dumpCache()618         static void dumpCache() {
619             if (DEBUG) {
620                 synchronized (sInstance) {
621                     LogTag.debug("Conversation dumpCache: ");
622                     for (Conversation c : sInstance.mCache) {
623                         LogTag.debug("   c: " + c + " c.getThreadId(): " + c.getThreadId() +
624                                 " hash: " + c.hashCode());
625                     }
626                 }
627             }
628         }
629 
630         /**
631          * Remove all conversations from the cache that are not in
632          * the provided set of thread IDs.
633          */
keepOnly(Set<Long> threads)634         static void keepOnly(Set<Long> threads) {
635             synchronized (sInstance) {
636                 Iterator<Conversation> iter = sInstance.mCache.iterator();
637                 while (iter.hasNext()) {
638                     Conversation c = iter.next();
639                     if (!threads.contains(c.getThreadId())) {
640                         iter.remove();
641                     }
642                 }
643             }
644         }
645     }
646 
647     /**
648      * Set up the conversation cache.  To be called once at application
649      * startup time.
650      */
init(final Context context)651     public static void init(final Context context) {
652         new Thread(new Runnable() {
653             public void run() {
654                 cacheAllThreads(context);
655             }
656         }).start();
657     }
658 
659     /**
660      * Are we in the process of loading and caching all the threads?.
661      */
loadingThreads()662    public static boolean loadingThreads() {
663        synchronized (Cache.getInstance()) {
664            return mLoadingThreads;
665        }
666     }
667 
cacheAllThreads(Context context)668    private static void cacheAllThreads(Context context) {
669        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
670            LogTag.debug("[Conversation] cacheAllThreads");
671        }
672        synchronized (Cache.getInstance()) {
673            if (mLoadingThreads) {
674               return;
675            }
676            mLoadingThreads = true;
677        }
678 
679        // Keep track of what threads are now on disk so we
680        // can discard anything removed from the cache.
681        HashSet<Long> threadsOnDisk = new HashSet<Long>();
682 
683        // Query for all conversations.
684        Cursor c = context.getContentResolver().query(sAllThreadsUri,
685                ALL_THREADS_PROJECTION, null, null, null);
686        try {
687            if (c != null) {
688                while (c.moveToNext()) {
689                    long threadId = c.getLong(ID);
690                    threadsOnDisk.add(threadId);
691 
692                    // Try to find this thread ID in the cache.
693                    Conversation conv;
694                    synchronized (Cache.getInstance()) {
695                        conv = Cache.get(threadId);
696                    }
697 
698                    if (conv == null) {
699                        // Make a new Conversation and put it in
700                        // the cache if necessary.
701                        conv = new Conversation(context, c, true);
702                        try {
703                            synchronized (Cache.getInstance()) {
704                                Cache.put(conv);
705                            }
706                        } catch (IllegalStateException e) {
707                            LogTag.error("Tried to add duplicate Conversation to Cache");
708                        }
709                    } else {
710                        // Or update in place so people with references
711                        // to conversations get updated too.
712                        fillFromCursor(context, conv, c, true);
713                    }
714                }
715            }
716        } finally {
717            if (c != null) {
718                c.close();
719            }
720            synchronized (Cache.getInstance()) {
721                mLoadingThreads = false;
722            }
723        }
724 
725        // Purge the cache of threads that no longer exist on disk.
726        Cache.keepOnly(threadsOnDisk);
727    }
728 
loadFromThreadId(long threadId)729     private boolean loadFromThreadId(long threadId) {
730         Cursor c = mContext.getContentResolver().query(sAllThreadsUri, ALL_THREADS_PROJECTION,
731                 "_id=" + Long.toString(threadId), null, null);
732         try {
733             if (c.moveToFirst()) {
734                 fillFromCursor(mContext, this, c, true);
735             } else {
736                 LogTag.error("loadFromThreadId: Can't find thread ID " + threadId);
737                 return false;
738             }
739         } finally {
740             c.close();
741         }
742         return true;
743     }
744 }
745