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