Contact.java revision fd644551e8506266aad2b76463b51b44154ed62f
1package com.android.mms.data; 2 3import java.io.IOException; 4import java.io.InputStream; 5import java.nio.CharBuffer; 6import java.util.ArrayList; 7import java.util.Arrays; 8import java.util.HashMap; 9import java.util.HashSet; 10import java.util.List; 11 12import android.content.ContentUris; 13import android.content.Context; 14import android.database.ContentObserver; 15import android.database.Cursor; 16import android.graphics.Bitmap; 17import android.graphics.BitmapFactory; 18import android.graphics.drawable.BitmapDrawable; 19import android.graphics.drawable.Drawable; 20import android.net.Uri; 21import android.os.Handler; 22import android.provider.ContactsContract.Contacts; 23import android.provider.ContactsContract.Data; 24import android.provider.ContactsContract.Presence; 25import android.provider.ContactsContract.CommonDataKinds.Email; 26import android.provider.ContactsContract.CommonDataKinds.Phone; 27import com.android.mmscommon.telephony.TelephonyProvider.Mms; 28import android.telephony.PhoneNumberUtils; 29import android.text.TextUtils; 30import android.util.Log; 31 32import android.database.sqlite.SqliteWrapper; 33import com.android.mms.ui.MessageUtils; 34import com.android.mms.LogTag; 35 36public class Contact { 37 private static final String TAG = "Contact"; 38 private static final boolean V = false; 39 private static ContactsCache sContactCache; 40 41// private static final ContentObserver sContactsObserver = new ContentObserver(new Handler()) { 42// @Override 43// public void onChange(boolean selfUpdate) { 44// if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 45// log("contact changed, invalidate cache"); 46// } 47// invalidateCache(); 48// } 49// }; 50 51 private static final ContentObserver sPresenceObserver = new ContentObserver(new Handler()) { 52 @Override 53 public void onChange(boolean selfUpdate) { 54 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 55 log("presence changed, invalidate cache"); 56 } 57 invalidateCache(); 58 } 59 }; 60 61 private final static HashSet<UpdateListener> mListeners = new HashSet<UpdateListener>(); 62 63 private String mNumber; 64 private String mName; 65 private String mNameAndNumber; // for display, e.g. Fred Flintstone <670-782-1123> 66 private boolean mNumberIsModified; // true if the number is modified 67 68 private long mRecipientId; // used to find the Recipient cache entry 69 private String mLabel; 70 private long mPersonId; 71 private int mPresenceResId; // TODO: make this a state instead of a res ID 72 private String mPresenceText; 73 private BitmapDrawable mAvatar; 74 private byte [] mAvatarData; 75 private boolean mIsStale; 76 private boolean mQueryPending; 77 78 public interface UpdateListener { 79 public void onUpdate(Contact updated); 80 } 81 82 /* 83 * Make a basic contact object with a phone number. 84 */ 85 private Contact(String number) { 86 mName = ""; 87 setNumber(number); 88 mNumberIsModified = false; 89 mLabel = ""; 90 mPersonId = 0; 91 mPresenceResId = 0; 92 mIsStale = true; 93 } 94 95 @Override 96 public String toString() { 97 return String.format("{ number=%s, name=%s, nameAndNumber=%s, label=%s, person_id=%d, hash=%d }", 98 mNumber, mName, mNameAndNumber, mLabel, mPersonId, hashCode()); 99 } 100 101 private static void logWithTrace(String msg, Object... format) { 102 Thread current = Thread.currentThread(); 103 StackTraceElement[] stack = current.getStackTrace(); 104 105 StringBuilder sb = new StringBuilder(); 106 sb.append("["); 107 sb.append(current.getId()); 108 sb.append("] "); 109 sb.append(String.format(msg, format)); 110 111 sb.append(" <- "); 112 int stop = stack.length > 7 ? 7 : stack.length; 113 for (int i = 3; i < stop; i++) { 114 String methodName = stack[i].getMethodName(); 115 sb.append(methodName); 116 if ((i+1) != stop) { 117 sb.append(" <- "); 118 } 119 } 120 121 Log.d(TAG, sb.toString()); 122 } 123 124 public static Contact get(String number, boolean canBlock) { 125 return sContactCache.get(number, canBlock); 126 } 127 128 public static void invalidateCache() { 129 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 130 log("invalidateCache"); 131 } 132 133 // While invalidating our local Cache doesn't remove the contacts, it will mark them 134 // stale so the next time we're asked for a particular contact, we'll return that 135 // stale contact and at the same time, fire off an asyncUpdateContact to update 136 // that contact's info in the background. UI elements using the contact typically 137 // call addListener() so they immediately get notified when the contact has been 138 // updated with the latest info. They redraw themselves when we call the 139 // listener's onUpdate(). 140 sContactCache.invalidate(); 141 } 142 143 private static String emptyIfNull(String s) { 144 return (s != null ? s : ""); 145 } 146 147 public static String formatNameAndNumber(String name, String number) { 148 // Format like this: Mike Cleron <(650) 555-1234> 149 // Erick Tseng <(650) 555-1212> 150 // Tutankhamun <tutank1341@gmail.com> 151 // (408) 555-1289 152 String formattedNumber = number; 153 if (!Mms.isEmailAddress(number)) { 154 formattedNumber = PhoneNumberUtils.formatNumber(number); 155 } 156 157 if (!TextUtils.isEmpty(name) && !name.equals(number)) { 158 return name + " <" + formattedNumber + ">"; 159 } else { 160 return formattedNumber; 161 } 162 } 163 164 public synchronized String getNumber() { 165 return mNumber; 166 } 167 168 public synchronized void setNumber(String number) { 169 mNumber = number; 170 notSynchronizedUpdateNameAndNumber(); 171 mNumberIsModified = true; 172 } 173 174 public boolean isNumberModified() { 175 return mNumberIsModified; 176 } 177 178 public void setIsNumberModified(boolean flag) { 179 mNumberIsModified = flag; 180 } 181 182 public synchronized String getName() { 183 if (TextUtils.isEmpty(mName)) { 184 return mNumber; 185 } else { 186 return mName; 187 } 188 } 189 190 public synchronized String getNameAndNumber() { 191 return mNameAndNumber; 192 } 193 194 private synchronized void updateNameAndNumber() { 195 notSynchronizedUpdateNameAndNumber(); 196 } 197 198 private void notSynchronizedUpdateNameAndNumber() { 199 mNameAndNumber = formatNameAndNumber(mName, mNumber); 200 } 201 202 public synchronized long getRecipientId() { 203 return mRecipientId; 204 } 205 206 public synchronized void setRecipientId(long id) { 207 mRecipientId = id; 208 } 209 210 public synchronized String getLabel() { 211 return mLabel; 212 } 213 214 public synchronized Uri getUri() { 215 return ContentUris.withAppendedId(Contacts.CONTENT_URI, mPersonId); 216 } 217 218 public synchronized int getPresenceResId() { 219 return mPresenceResId; 220 } 221 222 public synchronized boolean existsInDatabase() { 223 return (mPersonId > 0); 224 } 225 226 public static synchronized void addListener(UpdateListener l) { 227 mListeners.add(l); 228 } 229 230 public static synchronized void removeListener(UpdateListener l) { 231 mListeners.remove(l); 232 } 233 234 public static synchronized void dumpListeners() { 235 int i = 0; 236 Log.i(TAG, "[Contact] dumpListeners; size=" + mListeners.size()); 237 for (UpdateListener listener : mListeners) { 238 Log.i(TAG, "["+ (i++) + "]" + listener); 239 } 240 } 241 242 public synchronized boolean isEmail() { 243 return Mms.isEmailAddress(mNumber); 244 } 245 246 public String getPresenceText() { 247 return mPresenceText; 248 } 249 250 public synchronized Drawable getAvatar(Context context, Drawable defaultValue) { 251 if (mAvatar == null) { 252 if (mAvatarData != null) { 253 Bitmap b = BitmapFactory.decodeByteArray(mAvatarData, 0, mAvatarData.length); 254 mAvatar = new BitmapDrawable(context.getResources(), b); 255 } 256 } 257 return mAvatar != null ? mAvatar : defaultValue; 258 } 259 260 public static void init(final Context context) { 261 sContactCache = new ContactsCache(context); 262 263 RecipientIdCache.init(context); 264 265 // it maybe too aggressive to listen for *any* contact changes, and rebuild MMS contact 266 // cache each time that occurs. Unless we can get targeted updates for the contacts we 267 // care about(which probably won't happen for a long time), we probably should just 268 // invalidate cache peoridically, or surgically. 269 /* 270 context.getContentResolver().registerContentObserver( 271 Contacts.CONTENT_URI, true, sContactsObserver); 272 */ 273 } 274 275 public static void dump() { 276 sContactCache.dump(); 277 } 278 279 private static class ContactsCache { 280 private final TaskStack mTaskQueue = new TaskStack(); 281 private static final String SEPARATOR = ";"; 282 283 // query params for caller id lookup 284 private static final String CALLER_ID_SELECTION = "PHONE_NUMBERS_EQUAL(" + Phone.NUMBER 285 + ",?) AND " + Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'" 286 + " AND " + Data.RAW_CONTACT_ID + " IN " 287 + "(SELECT raw_contact_id " 288 + " FROM phone_lookup" 289 + " WHERE normalized_number GLOB('+*'))"; 290 291 // Utilizing private API 292 private static final Uri PHONES_WITH_PRESENCE_URI = Data.CONTENT_URI; 293 294 private static final String[] CALLER_ID_PROJECTION = new String[] { 295 Phone.NUMBER, // 0 296 Phone.LABEL, // 1 297 Phone.DISPLAY_NAME, // 2 298 Phone.CONTACT_ID, // 3 299 Phone.CONTACT_PRESENCE, // 4 300 Phone.CONTACT_STATUS, // 5 301 }; 302 303 private static final int PHONE_NUMBER_COLUMN = 0; 304 private static final int PHONE_LABEL_COLUMN = 1; 305 private static final int CONTACT_NAME_COLUMN = 2; 306 private static final int CONTACT_ID_COLUMN = 3; 307 private static final int CONTACT_PRESENCE_COLUMN = 4; 308 private static final int CONTACT_STATUS_COLUMN = 5; 309 310 // query params for contact lookup by email 311 private static final Uri EMAIL_WITH_PRESENCE_URI = Data.CONTENT_URI; 312 313 private static final String EMAIL_SELECTION = Email.DATA + "=? AND " + Data.MIMETYPE + "='" 314 + Email.CONTENT_ITEM_TYPE + "'"; 315 316 private static final String[] EMAIL_PROJECTION = new String[] { 317 Email.DISPLAY_NAME, // 0 318 Email.CONTACT_PRESENCE, // 1 319 Email.CONTACT_ID, // 2 320 Phone.DISPLAY_NAME, // 321 }; 322 private static final int EMAIL_NAME_COLUMN = 0; 323 private static final int EMAIL_STATUS_COLUMN = 1; 324 private static final int EMAIL_ID_COLUMN = 2; 325 private static final int EMAIL_CONTACT_NAME_COLUMN = 3; 326 327 private final Context mContext; 328 329 private final HashMap<String, ArrayList<Contact>> mContactsHash = 330 new HashMap<String, ArrayList<Contact>>(); 331 332 private ContactsCache(Context context) { 333 mContext = context; 334 } 335 336 void dump() { 337 synchronized (ContactsCache.this) { 338 Log.d(TAG, "**** Contact cache dump ****"); 339 for (String key : mContactsHash.keySet()) { 340 ArrayList<Contact> alc = mContactsHash.get(key); 341 for (Contact c : alc) { 342 Log.d(TAG, key + " ==> " + c.toString()); 343 } 344 } 345 } 346 } 347 348 private static class TaskStack { 349 Thread mWorkerThread; 350 private final ArrayList<Runnable> mThingsToLoad; 351 352 public TaskStack() { 353 mThingsToLoad = new ArrayList<Runnable>(); 354 mWorkerThread = new Thread(new Runnable() { 355 public void run() { 356 while (true) { 357 Runnable r = null; 358 synchronized (mThingsToLoad) { 359 if (mThingsToLoad.size() == 0) { 360 try { 361 mThingsToLoad.wait(); 362 } catch (InterruptedException ex) { 363 // nothing to do 364 } 365 } 366 if (mThingsToLoad.size() > 0) { 367 r = mThingsToLoad.remove(0); 368 } 369 } 370 if (r != null) { 371 r.run(); 372 } 373 } 374 } 375 }); 376 mWorkerThread.start(); 377 } 378 379 public void push(Runnable r) { 380 synchronized (mThingsToLoad) { 381 mThingsToLoad.add(r); 382 mThingsToLoad.notify(); 383 } 384 } 385 } 386 387 public void pushTask(Runnable r) { 388 mTaskQueue.push(r); 389 } 390 391 public Contact get(String number, boolean canBlock) { 392 if (V) logWithTrace("get(%s, %s)", number, canBlock); 393 394 if (TextUtils.isEmpty(number)) { 395 throw new IllegalArgumentException("Contact.get called with null or empty number"); 396 } 397 398 // Always return a Contact object, if if we don't have an actual contact 399 // in the contacts db. 400 Contact contact = get(number); 401 Runnable r = null; 402 403 synchronized (contact) { 404 // If there's a query pending and we're willing to block then 405 // wait here until the query completes. 406 while (canBlock && contact.mQueryPending) { 407 try { 408 contact.wait(); 409 } catch (InterruptedException ex) { 410 // try again by virtue of the loop unless mQueryPending is false 411 } 412 } 413 414 // If we're stale and we haven't already kicked off a query then kick 415 // it off here. 416 if (contact.mIsStale && !contact.mQueryPending) { 417 contact.mIsStale = false; 418 419 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 420 log("async update for " + contact.toString() + " canBlock: " + canBlock + 421 " isStale: " + contact.mIsStale); 422 } 423 424 final Contact c = contact; 425 r = new Runnable() { 426 public void run() { 427 updateContact(c); 428 } 429 }; 430 431 // set this to true while we have the lock on contact since we will 432 // either run the query directly (canBlock case) or push the query 433 // onto the queue. In either case the mQueryPending will get set 434 // to false via updateContact. 435 contact.mQueryPending = true; 436 } 437 } 438 // do this outside of the synchronized so we don't hold up any 439 // subsequent calls to "get" on other threads 440 if (r != null) { 441 if (canBlock) { 442 r.run(); 443 } else { 444 pushTask(r); 445 } 446 } 447 return contact; 448 } 449 450 private boolean contactChanged(Contact orig, Contact newContactData) { 451 // The phone number should never change, so don't bother checking. 452 // TODO: Maybe update it if it has gotten longer, i.e. 650-234-5678 -> +16502345678? 453 454 String oldName = emptyIfNull(orig.mName); 455 String newName = emptyIfNull(newContactData.mName); 456 if (!oldName.equals(newName)) { 457 if (V) Log.d(TAG, String.format("name changed: %s -> %s", oldName, newName)); 458 return true; 459 } 460 461 String oldLabel = emptyIfNull(orig.mLabel); 462 String newLabel = emptyIfNull(newContactData.mLabel); 463 if (!oldLabel.equals(newLabel)) { 464 if (V) Log.d(TAG, String.format("label changed: %s -> %s", oldLabel, newLabel)); 465 return true; 466 } 467 468 if (orig.mPersonId != newContactData.mPersonId) { 469 if (V) Log.d(TAG, "person id changed"); 470 return true; 471 } 472 473 if (orig.mPresenceResId != newContactData.mPresenceResId) { 474 if (V) Log.d(TAG, "presence changed"); 475 return true; 476 } 477 478 if (!Arrays.equals(orig.mAvatarData, newContactData.mAvatarData)) { 479 if (V) Log.d(TAG, "avatar changed"); 480 return true; 481 } 482 483 return false; 484 } 485 486 private void updateContact(final Contact c) { 487 if (c == null) { 488 return; 489 } 490 491 Contact entry = getContactInfo(c.mNumber); 492 synchronized (c) { 493 if (contactChanged(c, entry)) { 494 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 495 log("updateContact: contact changed for " + entry.mName); 496 } 497 498 c.mNumber = entry.mNumber; 499 c.mLabel = entry.mLabel; 500 c.mPersonId = entry.mPersonId; 501 c.mPresenceResId = entry.mPresenceResId; 502 c.mPresenceText = entry.mPresenceText; 503 c.mAvatarData = entry.mAvatarData; 504 c.mAvatar = entry.mAvatar; 505 506 // Check to see if this is the local ("me") number and update the name. 507 if (MessageUtils.isLocalNumber(c.mNumber)) { 508 c.mName = mContext.getString(com.android.mms.R.string.me); 509 } else { 510 c.mName = entry.mName; 511 } 512 513 c.notSynchronizedUpdateNameAndNumber(); 514 515 for (UpdateListener l : c.mListeners) { 516 if (V) Log.d(TAG, "updating " + l); 517 l.onUpdate(c); 518 } 519 } 520 synchronized (c) { 521 c.mQueryPending = false; 522 c.notifyAll(); 523 } 524 } 525 } 526 527 /** 528 * Returns the caller info in Contact. 529 */ 530 public Contact getContactInfo(String numberOrEmail) { 531 if (Mms.isEmailAddress(numberOrEmail)) { 532 return getContactInfoForEmailAddress(numberOrEmail); 533 } else { 534 return getContactInfoForPhoneNumber(numberOrEmail); 535 } 536 } 537 538 /** 539 * Queries the caller id info with the phone number. 540 * @return a Contact containing the caller id info corresponding to the number. 541 */ 542 private Contact getContactInfoForPhoneNumber(String number) { 543 number = PhoneNumberUtils.stripSeparators(number); 544 Contact entry = new Contact(number); 545 546 //if (LOCAL_DEBUG) log("queryContactInfoByNumber: number=" + number); 547 548 // We need to include the phone number in the selection string itself rather then 549 // selection arguments, because SQLite needs to see the exact pattern of GLOB 550 // to generate the correct query plan 551 String selection = CALLER_ID_SELECTION.replace("+", 552 PhoneNumberUtils.toCallerIDMinMatch(number)); 553 Cursor cursor = mContext.getContentResolver().query( 554 PHONES_WITH_PRESENCE_URI, 555 CALLER_ID_PROJECTION, 556 selection, 557 new String[] { number }, 558 null); 559 560 if (cursor == null) { 561 Log.w(TAG, "queryContactInfoByNumber(" + number + ") returned NULL cursor!" + 562 " contact uri used " + PHONES_WITH_PRESENCE_URI); 563 return entry; 564 } 565 566 try { 567 if (cursor.moveToFirst()) { 568 synchronized (entry) { 569 entry.mLabel = cursor.getString(PHONE_LABEL_COLUMN); 570 entry.mName = cursor.getString(CONTACT_NAME_COLUMN); 571 entry.mPersonId = cursor.getLong(CONTACT_ID_COLUMN); 572 entry.mPresenceResId = getPresenceIconResourceId( 573 cursor.getInt(CONTACT_PRESENCE_COLUMN)); 574 entry.mPresenceText = cursor.getString(CONTACT_STATUS_COLUMN); 575 if (V) { 576 log("queryContactInfoByNumber: name=" + entry.mName + 577 ", number=" + number + ", presence=" + entry.mPresenceResId); 578 } 579 } 580 581 byte[] data = loadAvatarData(entry); 582 583 synchronized (entry) { 584 entry.mAvatarData = data; 585 } 586 587 } 588 } finally { 589 cursor.close(); 590 } 591 592 return entry; 593 } 594 595 /* 596 * Load the avatar data from the cursor into memory. Don't decode the data 597 * until someone calls for it (see getAvatar). Hang onto the raw data so that 598 * we can compare it when the data is reloaded. 599 * TODO: consider comparing a checksum so that we don't have to hang onto 600 * the raw bytes after the image is decoded. 601 */ 602 private byte[] loadAvatarData(Contact entry) { 603 byte [] data = null; 604 605 if (entry.mPersonId == 0 || entry.mAvatar != null) { 606 return null; 607 } 608 609 Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, entry.mPersonId); 610 611 InputStream avatarDataStream = Contacts.openContactPhotoInputStream( 612 mContext.getContentResolver(), 613 contactUri); 614 try { 615 if (avatarDataStream != null) { 616 data = new byte[avatarDataStream.available()]; 617 avatarDataStream.read(data, 0, data.length); 618 } 619 } catch (IOException ex) { 620 // 621 } finally { 622 try { 623 if (avatarDataStream != null) { 624 avatarDataStream.close(); 625 } 626 } catch (IOException e) { 627 } 628 } 629 630 return data; 631 } 632 633 private int getPresenceIconResourceId(int presence) { 634 // TODO: must fix for SDK 635 if (presence != Presence.OFFLINE) { 636 return Presence.getPresenceIconResourceId(presence); 637 } 638 639 return 0; 640 } 641 642 /** 643 * Query the contact email table to get the name of an email address. 644 */ 645 private Contact getContactInfoForEmailAddress(String email) { 646 Contact entry = new Contact(email); 647 648 Cursor cursor = SqliteWrapper.query(mContext, mContext.getContentResolver(), 649 EMAIL_WITH_PRESENCE_URI, 650 EMAIL_PROJECTION, 651 EMAIL_SELECTION, 652 new String[] { email }, 653 null); 654 655 if (cursor != null) { 656 try { 657 while (cursor.moveToNext()) { 658 boolean found = false; 659 660 synchronized (entry) { 661 entry.mPresenceResId = getPresenceIconResourceId( 662 cursor.getInt(EMAIL_STATUS_COLUMN)); 663 entry.mPersonId = cursor.getLong(EMAIL_ID_COLUMN); 664 665 String name = cursor.getString(EMAIL_NAME_COLUMN); 666 if (TextUtils.isEmpty(name)) { 667 name = cursor.getString(EMAIL_CONTACT_NAME_COLUMN); 668 } 669 if (!TextUtils.isEmpty(name)) { 670 entry.mName = name; 671 if (V) { 672 log("getContactInfoForEmailAddress: name=" + entry.mName + 673 ", email=" + email + ", presence=" + 674 entry.mPresenceResId); 675 } 676 found = true; 677 } 678 } 679 680 if (found) { 681 byte[] data = loadAvatarData(entry); 682 synchronized (entry) { 683 entry.mAvatarData = data; 684 } 685 686 break; 687 } 688 } 689 } finally { 690 cursor.close(); 691 } 692 } 693 return entry; 694 } 695 696 // Invert and truncate to five characters the phoneNumber so that we 697 // can use it as the key in a hashtable. We keep a mapping of this 698 // key to a list of all contacts which have the same key. 699 private String key(String phoneNumber, CharBuffer keyBuffer) { 700 keyBuffer.clear(); 701 keyBuffer.mark(); 702 703 int position = phoneNumber.length(); 704 int resultCount = 0; 705 while (--position >= 0) { 706 char c = phoneNumber.charAt(position); 707 if (Character.isDigit(c)) { 708 keyBuffer.put(c); 709 if (++resultCount == STATIC_KEY_BUFFER_MAXIMUM_LENGTH) { 710 break; 711 } 712 } 713 } 714 keyBuffer.reset(); 715 if (resultCount > 0) { 716 return keyBuffer.toString(); 717 } else { 718 // there were no usable digits in the input phoneNumber 719 return phoneNumber; 720 } 721 } 722 723 // Reuse this so we don't have to allocate each time we go through this 724 // "get" function. 725 static final int STATIC_KEY_BUFFER_MAXIMUM_LENGTH = 5; 726 static CharBuffer sStaticKeyBuffer = CharBuffer.allocate(STATIC_KEY_BUFFER_MAXIMUM_LENGTH); 727 728 public Contact get(String numberOrEmail) { 729 synchronized (ContactsCache.this) { 730 // See if we can find "number" in the hashtable. 731 // If so, just return the result. 732 final boolean isNotRegularPhoneNumber = Mms.isEmailAddress(numberOrEmail) || 733 MessageUtils.isAlias(numberOrEmail); 734 final String key = isNotRegularPhoneNumber ? 735 numberOrEmail : key(numberOrEmail, sStaticKeyBuffer); 736 737 ArrayList<Contact> candidates = mContactsHash.get(key); 738 if (candidates != null) { 739 int length = candidates.size(); 740 for (int i = 0; i < length; i++) { 741 Contact c= candidates.get(i); 742 if (isNotRegularPhoneNumber) { 743 if (numberOrEmail.equals(c.mNumber)) { 744 return c; 745 } 746 } else { 747 if (PhoneNumberUtils.compare(numberOrEmail, c.mNumber)) { 748 return c; 749 } 750 } 751 } 752 } else { 753 candidates = new ArrayList<Contact>(); 754 // call toString() since it may be the static CharBuffer 755 mContactsHash.put(key, candidates); 756 } 757 Contact c = new Contact(numberOrEmail); 758 candidates.add(c); 759 return c; 760 } 761 } 762 763 void invalidate() { 764 // Don't remove the contacts. Just mark them stale so we'll update their 765 // info, particularly their presence. 766 synchronized (ContactsCache.this) { 767 for (ArrayList<Contact> alc : mContactsHash.values()) { 768 for (Contact c : alc) { 769 synchronized (c) { 770 c.mIsStale = true; 771 } 772 } 773 } 774 } 775 } 776 } 777 778 private static void log(String msg) { 779 Log.d(TAG, msg); 780 } 781} 782