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