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.database.sqlite.SqliteWrapper; 17 import android.graphics.Bitmap; 18 import android.graphics.BitmapFactory; 19 import android.graphics.drawable.BitmapDrawable; 20 import android.graphics.drawable.Drawable; 21 import android.net.Uri; 22 import android.os.Handler; 23 import android.os.Parcelable; 24 import android.provider.ContactsContract.CommonDataKinds.Email; 25 import android.provider.ContactsContract.CommonDataKinds.Phone; 26 import android.provider.ContactsContract.Contacts; 27 import android.provider.ContactsContract.Data; 28 import android.provider.ContactsContract.Presence; 29 import android.provider.ContactsContract.Profile; 30 import android.provider.Telephony.Mms; 31 import android.telephony.PhoneNumberUtils; 32 import android.text.TextUtils; 33 import android.util.Log; 34 35 import com.android.mms.LogTag; 36 import com.android.mms.MmsApp; 37 import com.android.mms.R; 38 import com.android.mms.ui.MessageUtils; 39 40 public class Contact { 41 public static final int CONTACT_METHOD_TYPE_UNKNOWN = 0; 42 public static final int CONTACT_METHOD_TYPE_PHONE = 1; 43 public static final int CONTACT_METHOD_TYPE_EMAIL = 2; 44 public static final int CONTACT_METHOD_TYPE_SELF = 3; // the "Me" or profile contact 45 public static final String TEL_SCHEME = "tel"; 46 public static final String CONTENT_SCHEME = "content"; 47 private static final int CONTACT_METHOD_ID_UNKNOWN = -1; 48 private static final String TAG = LogTag.TAG; 49 private static ContactsCache sContactCache; 50 private static final String SELF_ITEM_KEY = "Self_Item_Key"; 51 52 // private static final ContentObserver sContactsObserver = new ContentObserver(new Handler()) { 53 // @Override 54 // public void onChange(boolean selfUpdate) { 55 // if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 56 // log("contact changed, invalidate cache"); 57 // } 58 // invalidateCache(); 59 // } 60 // }; 61 62 private static final ContentObserver sPresenceObserver = new ContentObserver(new Handler()) { 63 @Override 64 public void onChange(boolean selfUpdate) { 65 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 66 log("presence changed, invalidate cache"); 67 } 68 invalidateCache(); 69 } 70 }; 71 72 private final static HashSet<UpdateListener> mListeners = new HashSet<UpdateListener>(); 73 74 private long mContactMethodId; // Id in phone or email Uri returned by provider of current 75 // Contact, -1 is invalid. e.g. contact method id is 20 when 76 // current contact has phone content://.../phones/20. 77 private int mContactMethodType; 78 private String mNumber; 79 private String mNumberE164; 80 private String mName; 81 private String mNameAndNumber; // for display, e.g. Fred Flintstone <670-782-1123> 82 private boolean mNumberIsModified; // true if the number is modified 83 84 private long mRecipientId; // used to find the Recipient cache entry 85 private String mLabel; 86 private long mPersonId; 87 private int mPresenceResId; // TODO: make this a state instead of a res ID 88 private String mPresenceText; 89 private BitmapDrawable mAvatar; 90 private byte [] mAvatarData; 91 private boolean mIsStale; 92 private boolean mQueryPending; 93 private boolean mIsMe; // true if this contact is me! 94 private boolean mSendToVoicemail; // true if this contact should not put up notification 95 private Uri mPeopleReferenceUri; 96 97 public interface UpdateListener { onUpdate(Contact updated)98 public void onUpdate(Contact updated); 99 } 100 Contact(String number, String name)101 private Contact(String number, String name) { 102 init(number, name); 103 } 104 /* 105 * Make a basic contact object with a phone number. 106 */ Contact(String number)107 private Contact(String number) { 108 init(number, ""); 109 } 110 Contact(boolean isMe)111 private Contact(boolean isMe) { 112 init(SELF_ITEM_KEY, ""); 113 mIsMe = isMe; 114 } 115 init(String number, String name)116 private void init(String number, String name) { 117 mContactMethodId = CONTACT_METHOD_ID_UNKNOWN; 118 mName = name; 119 setNumber(number); 120 mNumberIsModified = false; 121 mLabel = ""; 122 mPersonId = 0; 123 mPresenceResId = 0; 124 mIsStale = true; 125 mSendToVoicemail = false; 126 } 127 @Override toString()128 public String toString() { 129 return String.format("{ number=%s, name=%s, nameAndNumber=%s, label=%s, person_id=%d, hash=%d method_id=%d }", 130 (mNumber != null ? mNumber : "null"), 131 (mName != null ? mName : "null"), 132 (mNameAndNumber != null ? mNameAndNumber : "null"), 133 (mLabel != null ? mLabel : "null"), 134 mPersonId, hashCode(), 135 mContactMethodId); 136 } 137 logWithTrace(String tag, String msg, Object... format)138 public static void logWithTrace(String tag, String msg, Object... format) { 139 Thread current = Thread.currentThread(); 140 StackTraceElement[] stack = current.getStackTrace(); 141 142 StringBuilder sb = new StringBuilder(); 143 sb.append("["); 144 sb.append(current.getId()); 145 sb.append("] "); 146 sb.append(String.format(msg, format)); 147 148 sb.append(" <- "); 149 int stop = stack.length > 7 ? 7 : stack.length; 150 for (int i = 3; i < stop; i++) { 151 String methodName = stack[i].getMethodName(); 152 sb.append(methodName); 153 if ((i+1) != stop) { 154 sb.append(" <- "); 155 } 156 } 157 158 Log.d(tag, sb.toString()); 159 } 160 get(String number, boolean canBlock)161 public static Contact get(String number, boolean canBlock) { 162 return sContactCache.get(number, canBlock); 163 } 164 getMe(boolean canBlock)165 public static Contact getMe(boolean canBlock) { 166 return sContactCache.getMe(canBlock); 167 } 168 removeFromCache()169 public void removeFromCache() { 170 sContactCache.remove(this); 171 } 172 getByPhoneUris(Parcelable[] uris)173 public static List<Contact> getByPhoneUris(Parcelable[] uris) { 174 return sContactCache.getContactInfoForPhoneUris(uris); 175 } 176 invalidateCache()177 public static void invalidateCache() { 178 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 179 log("invalidateCache"); 180 } 181 182 // While invalidating our local Cache doesn't remove the contacts, it will mark them 183 // stale so the next time we're asked for a particular contact, we'll return that 184 // stale contact and at the same time, fire off an asyncUpdateContact to update 185 // that contact's info in the background. UI elements using the contact typically 186 // call addListener() so they immediately get notified when the contact has been 187 // updated with the latest info. They redraw themselves when we call the 188 // listener's onUpdate(). 189 sContactCache.invalidate(); 190 } 191 isMe()192 public boolean isMe() { 193 return mIsMe; 194 } 195 emptyIfNull(String s)196 private static String emptyIfNull(String s) { 197 return (s != null ? s : ""); 198 } 199 200 /** 201 * Fomat the name and number. 202 * 203 * @param name 204 * @param number 205 * @param numberE164 the number's E.164 representation, is used to get the 206 * country the number belongs to. 207 * @return the formatted name and number 208 */ formatNameAndNumber(String name, String number, String numberE164)209 public static String formatNameAndNumber(String name, String number, String numberE164) { 210 // Format like this: Mike Cleron <(650) 555-1234> 211 // Erick Tseng <(650) 555-1212> 212 // Tutankhamun <tutank1341@gmail.com> 213 // (408) 555-1289 214 String formattedNumber = number; 215 if (!Mms.isEmailAddress(number)) { 216 formattedNumber = PhoneNumberUtils.formatNumber(number, numberE164, 217 MmsApp.getApplication().getCurrentCountryIso()); 218 } 219 220 if (!TextUtils.isEmpty(name) && !name.equals(number)) { 221 return name + " <" + formattedNumber + ">"; 222 } else { 223 return formattedNumber; 224 } 225 } 226 reload()227 public synchronized void reload() { 228 mIsStale = true; 229 sContactCache.get(mNumber, false); 230 } 231 getNumber()232 public synchronized String getNumber() { 233 return mNumber; 234 } 235 setNumber(String number)236 public synchronized void setNumber(String number) { 237 if (!Mms.isEmailAddress(number)) { 238 mNumber = PhoneNumberUtils.formatNumber(number, mNumberE164, 239 MmsApp.getApplication().getCurrentCountryIso()); 240 } else { 241 mNumber = number; 242 } 243 notSynchronizedUpdateNameAndNumber(); 244 mNumberIsModified = true; 245 } 246 isNumberModified()247 public boolean isNumberModified() { 248 return mNumberIsModified; 249 } 250 getSendToVoicemail()251 public boolean getSendToVoicemail() { 252 return mSendToVoicemail; 253 } 254 setIsNumberModified(boolean flag)255 public void setIsNumberModified(boolean flag) { 256 mNumberIsModified = flag; 257 } 258 getName()259 public synchronized String getName() { 260 if (TextUtils.isEmpty(mName)) { 261 return mNumber; 262 } else { 263 return mName; 264 } 265 } 266 getNameAndNumber()267 public synchronized String getNameAndNumber() { 268 return mNameAndNumber; 269 } 270 notSynchronizedUpdateNameAndNumber()271 private void notSynchronizedUpdateNameAndNumber() { 272 mNameAndNumber = formatNameAndNumber(mName, mNumber, mNumberE164); 273 } 274 getRecipientId()275 public synchronized long getRecipientId() { 276 return mRecipientId; 277 } 278 setRecipientId(long id)279 public synchronized void setRecipientId(long id) { 280 mRecipientId = id; 281 } 282 getLabel()283 public synchronized String getLabel() { 284 return mLabel; 285 } 286 getUri()287 public synchronized Uri getUri() { 288 return ContentUris.withAppendedId(Contacts.CONTENT_URI, mPersonId); 289 } 290 getPresenceResId()291 public synchronized int getPresenceResId() { 292 return mPresenceResId; 293 } 294 existsInDatabase()295 public synchronized boolean existsInDatabase() { 296 return (mPersonId > 0); 297 } 298 addListener(UpdateListener l)299 public static void addListener(UpdateListener l) { 300 synchronized (mListeners) { 301 mListeners.add(l); 302 } 303 } 304 removeListener(UpdateListener l)305 public static void removeListener(UpdateListener l) { 306 synchronized (mListeners) { 307 mListeners.remove(l); 308 } 309 } 310 dumpListeners()311 public static void dumpListeners() { 312 synchronized (mListeners) { 313 int i = 0; 314 Log.i(TAG, "[Contact] dumpListeners; size=" + mListeners.size()); 315 for (UpdateListener listener : mListeners) { 316 Log.i(TAG, "["+ (i++) + "]" + listener); 317 } 318 } 319 } 320 isEmail()321 public synchronized boolean isEmail() { 322 return Mms.isEmailAddress(mNumber); 323 } 324 getPresenceText()325 public String getPresenceText() { 326 return mPresenceText; 327 } 328 getContactMethodType()329 public int getContactMethodType() { 330 return mContactMethodType; 331 } 332 getPeopleReferenceUri()333 public Uri getPeopleReferenceUri() { 334 return mPeopleReferenceUri; 335 } 336 getContactMethodId()337 public long getContactMethodId() { 338 return mContactMethodId; 339 } 340 getPhoneUri()341 public synchronized Uri getPhoneUri() { 342 if (existsInDatabase()) { 343 return ContentUris.withAppendedId(Phone.CONTENT_URI, mContactMethodId); 344 } else { 345 Uri.Builder ub = new Uri.Builder(); 346 ub.scheme(TEL_SCHEME); 347 ub.encodedOpaquePart(mNumber); 348 return ub.build(); 349 } 350 } 351 getAvatar(Context context, Drawable defaultValue)352 public synchronized Drawable getAvatar(Context context, Drawable defaultValue) { 353 if (mAvatar == null) { 354 if (mAvatarData != null) { 355 Bitmap b = BitmapFactory.decodeByteArray(mAvatarData, 0, mAvatarData.length); 356 mAvatar = new BitmapDrawable(context.getResources(), b); 357 } 358 } 359 return mAvatar != null ? mAvatar : defaultValue; 360 } 361 init(final Context context)362 public static void init(final Context context) { 363 if (sContactCache != null) { // Stop previous Runnable 364 sContactCache.mTaskQueue.mWorkerThread.interrupt(); 365 } 366 sContactCache = new ContactsCache(context); 367 368 RecipientIdCache.init(context); 369 370 // it maybe too aggressive to listen for *any* contact changes, and rebuild MMS contact 371 // cache each time that occurs. Unless we can get targeted updates for the contacts we 372 // care about(which probably won't happen for a long time), we probably should just 373 // invalidate cache peoridically, or surgically. 374 /* 375 context.getContentResolver().registerContentObserver( 376 Contacts.CONTENT_URI, true, sContactsObserver); 377 */ 378 } 379 dump()380 public static void dump() { 381 sContactCache.dump(); 382 } 383 384 private static class ContactsCache { 385 private final TaskStack mTaskQueue = new TaskStack(); 386 private static final String SEPARATOR = ";"; 387 388 /** 389 * For a specified phone number, 2 rows were inserted into phone_lookup 390 * table. One is the phone number's E164 representation, and another is 391 * one's normalized format. If the phone number's normalized format in 392 * the lookup table is the suffix of the given number's one, it is 393 * treated as matched CallerId. E164 format number must fully equal. 394 * 395 * For example: Both 650-123-4567 and +1 (650) 123-4567 will match the 396 * normalized number 6501234567 in the phone lookup. 397 * 398 * The min_match is used to narrow down the candidates for the final 399 * comparison. 400 */ 401 // query params for caller id lookup 402 private static final String CALLER_ID_SELECTION = " Data._ID IN " 403 + " (SELECT DISTINCT lookup.data_id " 404 + " FROM " 405 + " (SELECT data_id, normalized_number, length(normalized_number) as len " 406 + " FROM phone_lookup " 407 + " WHERE min_match = ?) AS lookup " 408 + " WHERE lookup.normalized_number = ? OR" 409 + " (lookup.len <= ? AND " 410 + " substr(?, ? - lookup.len + 1) = lookup.normalized_number))"; 411 412 // query params for caller id lookup without E164 number as param 413 private static final String CALLER_ID_SELECTION_WITHOUT_E164 = " Data._ID IN " 414 + " (SELECT DISTINCT lookup.data_id " 415 + " FROM " 416 + " (SELECT data_id, normalized_number, length(normalized_number) as len " 417 + " FROM phone_lookup " 418 + " WHERE min_match = ?) AS lookup " 419 + " WHERE " 420 + " (lookup.len <= ? AND " 421 + " substr(?, ? - lookup.len + 1) = lookup.normalized_number))"; 422 423 // Utilizing private API 424 private static final Uri PHONES_WITH_PRESENCE_URI = Data.CONTENT_URI; 425 426 private static final String[] CALLER_ID_PROJECTION = new String[] { 427 Phone._ID, // 0 428 Phone.NUMBER, // 1 429 Phone.LABEL, // 2 430 Phone.DISPLAY_NAME, // 3 431 Phone.CONTACT_ID, // 4 432 Phone.CONTACT_PRESENCE, // 5 433 Phone.CONTACT_STATUS, // 6 434 Phone.NORMALIZED_NUMBER, // 7 435 Contacts.SEND_TO_VOICEMAIL // 8 436 }; 437 438 private static final int PHONE_ID_COLUMN = 0; 439 private static final int PHONE_NUMBER_COLUMN = 1; 440 private static final int PHONE_LABEL_COLUMN = 2; 441 private static final int CONTACT_NAME_COLUMN = 3; 442 private static final int CONTACT_ID_COLUMN = 4; 443 private static final int CONTACT_PRESENCE_COLUMN = 5; 444 private static final int CONTACT_STATUS_COLUMN = 6; 445 private static final int PHONE_NORMALIZED_NUMBER = 7; 446 private static final int SEND_TO_VOICEMAIL = 8; 447 448 private static final String[] SELF_PROJECTION = new String[] { 449 Phone._ID, // 0 450 Phone.DISPLAY_NAME, // 1 451 }; 452 453 private static final int SELF_ID_COLUMN = 0; 454 private static final int SELF_NAME_COLUMN = 1; 455 456 // query params for contact lookup by email 457 private static final Uri EMAIL_WITH_PRESENCE_URI = Data.CONTENT_URI; 458 459 private static final String EMAIL_SELECTION = "UPPER(" + Email.DATA + ")=UPPER(?) AND " 460 + Data.MIMETYPE + "='" + Email.CONTENT_ITEM_TYPE + "'"; 461 462 private static final String[] EMAIL_PROJECTION = new String[] { 463 Email._ID, // 0 464 Email.DISPLAY_NAME, // 1 465 Email.CONTACT_PRESENCE, // 2 466 Email.CONTACT_ID, // 3 467 Phone.DISPLAY_NAME, // 4 468 Contacts.SEND_TO_VOICEMAIL // 5 469 }; 470 private static final int EMAIL_ID_COLUMN = 0; 471 private static final int EMAIL_NAME_COLUMN = 1; 472 private static final int EMAIL_STATUS_COLUMN = 2; 473 private static final int EMAIL_CONTACT_ID_COLUMN = 3; 474 private static final int EMAIL_CONTACT_NAME_COLUMN = 4; 475 private static final int EMAIL_SEND_TO_VOICEMAIL_COLUMN = 5; 476 477 private final Context mContext; 478 479 private final HashMap<String, ArrayList<Contact>> mContactsHash = 480 new HashMap<String, ArrayList<Contact>>(); 481 ContactsCache(Context context)482 private ContactsCache(Context context) { 483 mContext = context; 484 } 485 dump()486 void dump() { 487 synchronized (ContactsCache.this) { 488 Log.d(TAG, "**** Contact cache dump ****"); 489 for (String key : mContactsHash.keySet()) { 490 ArrayList<Contact> alc = mContactsHash.get(key); 491 for (Contact c : alc) { 492 Log.d(TAG, key + " ==> " + c.toString()); 493 } 494 } 495 } 496 } 497 498 private static class TaskStack { 499 Thread mWorkerThread; 500 private final ArrayList<Runnable> mThingsToLoad; 501 TaskStack()502 public TaskStack() { 503 mThingsToLoad = new ArrayList<Runnable>(); 504 mWorkerThread = new Thread(new Runnable() { 505 @Override 506 public void run() { 507 while (true) { 508 Runnable r = null; 509 synchronized (mThingsToLoad) { 510 if (mThingsToLoad.size() == 0) { 511 try { 512 mThingsToLoad.wait(); 513 } catch (InterruptedException ex) { 514 break; // Exception sent by Contact.init() to stop Runnable 515 } 516 } 517 if (mThingsToLoad.size() > 0) { 518 r = mThingsToLoad.remove(0); 519 } 520 } 521 if (r != null) { 522 r.run(); 523 } 524 } 525 } 526 }, "Contact.ContactsCache.TaskStack worker thread"); 527 mWorkerThread.setPriority(Thread.MIN_PRIORITY); 528 mWorkerThread.start(); 529 } 530 push(Runnable r)531 public void push(Runnable r) { 532 synchronized (mThingsToLoad) { 533 mThingsToLoad.add(r); 534 mThingsToLoad.notify(); 535 } 536 } 537 } 538 pushTask(Runnable r)539 public void pushTask(Runnable r) { 540 mTaskQueue.push(r); 541 } 542 getMe(boolean canBlock)543 public Contact getMe(boolean canBlock) { 544 return get(SELF_ITEM_KEY, true, canBlock); 545 } 546 get(String number, boolean canBlock)547 public Contact get(String number, boolean canBlock) { 548 return get(number, false, canBlock); 549 } 550 get(String number, boolean isMe, boolean canBlock)551 private Contact get(String number, boolean isMe, boolean canBlock) { 552 if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { 553 logWithTrace(TAG, "get(%s, %s, %s)", number, isMe, canBlock); 554 } 555 556 if (TextUtils.isEmpty(number)) { 557 number = ""; // In some places (such as Korea), it's possible to receive 558 // a message without the sender's address. In this case, 559 // all such anonymous messages will get added to the same 560 // thread. 561 } 562 563 // Always return a Contact object, if if we don't have an actual contact 564 // in the contacts db. 565 Contact contact = internalGet(number, isMe); 566 Runnable r = null; 567 568 synchronized (contact) { 569 // If there's a query pending and we're willing to block then 570 // wait here until the query completes. 571 while (canBlock && contact.mQueryPending) { 572 try { 573 contact.wait(); 574 } catch (InterruptedException ex) { 575 // try again by virtue of the loop unless mQueryPending is false 576 } 577 } 578 579 // If we're stale and we haven't already kicked off a query then kick 580 // it off here. 581 if (contact.mIsStale && !contact.mQueryPending) { 582 contact.mIsStale = false; 583 584 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 585 log("async update for " + contact.toString() + " canBlock: " + canBlock + 586 " isStale: " + contact.mIsStale); 587 } 588 589 final Contact c = contact; 590 r = new Runnable() { 591 @Override 592 public void run() { 593 updateContact(c); 594 } 595 }; 596 597 // set this to true while we have the lock on contact since we will 598 // either run the query directly (canBlock case) or push the query 599 // onto the queue. In either case the mQueryPending will get set 600 // to false via updateContact. 601 contact.mQueryPending = true; 602 } 603 } 604 // do this outside of the synchronized so we don't hold up any 605 // subsequent calls to "get" on other threads 606 if (r != null) { 607 if (canBlock) { 608 r.run(); 609 } else { 610 pushTask(r); 611 } 612 } 613 return contact; 614 } 615 616 /** 617 * Get CacheEntry list for given phone URIs. This method will do single one query to 618 * get expected contacts from provider. Be sure passed in URIs are not null and contains 619 * only valid URIs. 620 */ getContactInfoForPhoneUris(Parcelable[] uris)621 public List<Contact> getContactInfoForPhoneUris(Parcelable[] uris) { 622 if (uris.length == 0) { 623 return null; 624 } 625 StringBuilder idSetBuilder = new StringBuilder(); 626 boolean first = true; 627 for (Parcelable p : uris) { 628 Uri uri = (Uri) p; 629 if ("content".equals(uri.getScheme())) { 630 if (first) { 631 first = false; 632 idSetBuilder.append(uri.getLastPathSegment()); 633 } else { 634 idSetBuilder.append(',').append(uri.getLastPathSegment()); 635 } 636 } 637 } 638 // Check whether there is content URI. 639 if (first) return null; 640 Cursor cursor = null; 641 if (idSetBuilder.length() > 0) { 642 final String whereClause = Phone._ID + " IN (" + idSetBuilder.toString() + ")"; 643 cursor = mContext.getContentResolver().query( 644 PHONES_WITH_PRESENCE_URI, CALLER_ID_PROJECTION, whereClause, null, null); 645 } 646 647 if (cursor == null) { 648 return null; 649 } 650 651 List<Contact> entries = new ArrayList<Contact>(); 652 653 try { 654 while (cursor.moveToNext()) { 655 Contact entry = new Contact(cursor.getString(PHONE_NUMBER_COLUMN), 656 cursor.getString(CONTACT_NAME_COLUMN)); 657 fillPhoneTypeContact(entry, cursor); 658 ArrayList<Contact> value = new ArrayList<Contact>(); 659 value.add(entry); 660 // Put the result in the cache. 661 mContactsHash.put(key(entry.mNumber, sStaticKeyBuffer), value); 662 entries.add(entry); 663 } 664 } finally { 665 cursor.close(); 666 } 667 return entries; 668 } 669 contactChanged(Contact orig, Contact newContactData)670 private boolean contactChanged(Contact orig, Contact newContactData) { 671 // The phone number should never change, so don't bother checking. 672 // TODO: Maybe update it if it has gotten longer, i.e. 650-234-5678 -> +16502345678? 673 674 // Do the quick check first. 675 if (orig.mContactMethodType != newContactData.mContactMethodType) { 676 return true; 677 } 678 679 if (orig.mContactMethodId != newContactData.mContactMethodId) { 680 return true; 681 } 682 683 if (orig.mPersonId != newContactData.mPersonId) { 684 if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { 685 Log.d(TAG, "person id changed"); 686 } 687 return true; 688 } 689 690 if (orig.mPresenceResId != newContactData.mPresenceResId) { 691 if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { 692 Log.d(TAG, "presence changed"); 693 } 694 return true; 695 } 696 697 if (orig.mSendToVoicemail != newContactData.mSendToVoicemail) { 698 return true; 699 } 700 701 String oldName = emptyIfNull(orig.mName); 702 String newName = emptyIfNull(newContactData.mName); 703 if (!oldName.equals(newName)) { 704 if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { 705 Log.d(TAG, String.format("name changed: %s -> %s", oldName, newName)); 706 } 707 return true; 708 } 709 710 String oldLabel = emptyIfNull(orig.mLabel); 711 String newLabel = emptyIfNull(newContactData.mLabel); 712 if (!oldLabel.equals(newLabel)) { 713 if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { 714 Log.d(TAG, String.format("label changed: %s -> %s", oldLabel, newLabel)); 715 } 716 return true; 717 } 718 719 if (!Arrays.equals(orig.mAvatarData, newContactData.mAvatarData)) { 720 if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { 721 Log.d(TAG, "avatar changed"); 722 } 723 return true; 724 } 725 726 return false; 727 } 728 updateContact(final Contact c)729 private void updateContact(final Contact c) { 730 if (c == null) { 731 return; 732 } 733 734 Contact entry = getContactInfo(c); 735 synchronized (c) { 736 if (contactChanged(c, entry)) { 737 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 738 log("updateContact: contact changed for " + entry.mName); 739 } 740 741 c.mNumber = entry.mNumber; 742 c.mLabel = entry.mLabel; 743 c.mPersonId = entry.mPersonId; 744 c.mPresenceResId = entry.mPresenceResId; 745 c.mPresenceText = entry.mPresenceText; 746 c.mAvatarData = entry.mAvatarData; 747 c.mAvatar = entry.mAvatar; 748 c.mContactMethodId = entry.mContactMethodId; 749 c.mContactMethodType = entry.mContactMethodType; 750 c.mNumberE164 = entry.mNumberE164; 751 c.mName = entry.mName; 752 c.mSendToVoicemail = entry.mSendToVoicemail; 753 c.mPeopleReferenceUri = entry.mPeopleReferenceUri; 754 755 c.notSynchronizedUpdateNameAndNumber(); 756 757 // We saw a bug where we were updating an empty contact. That would trigger 758 // l.onUpdate() below, which would call ComposeMessageActivity.onUpdate, 759 // which would call the adapter's notifyDataSetChanged, which would throw 760 // away the message items and rebuild, eventually calling updateContact() 761 // again -- all in a vicious and unending loop. Break the cycle and don't 762 // notify if the number (the most important piece of information) is empty. 763 if (!TextUtils.isEmpty(c.mNumber)) { 764 // clone the list of listeners in case the onUpdate call turns around and 765 // modifies the list of listeners 766 // access to mListeners is synchronized on ContactsCache 767 HashSet<UpdateListener> iterator; 768 synchronized (mListeners) { 769 iterator = (HashSet<UpdateListener>)Contact.mListeners.clone(); 770 } 771 for (UpdateListener l : iterator) { 772 if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { 773 Log.d(TAG, "updating " + l); 774 } 775 l.onUpdate(c); 776 } 777 } 778 } 779 synchronized (c) { 780 c.mQueryPending = false; 781 c.notifyAll(); 782 } 783 } 784 } 785 786 /** 787 * Returns the caller info in Contact. 788 */ getContactInfo(Contact c)789 private Contact getContactInfo(Contact c) { 790 if (c.mIsMe) { 791 return getContactInfoForSelf(); 792 } else if (Mms.isEmailAddress(c.mNumber)) { 793 return getContactInfoForEmailAddress(c.mNumber); 794 } else if (isAlphaNumber(c.mNumber)) { 795 // first try to look it up in the email field 796 Contact contact = getContactInfoForEmailAddress(c.mNumber); 797 if (contact.existsInDatabase()) { 798 return contact; 799 } 800 // then look it up in the phone field 801 return getContactInfoForPhoneNumber(c.mNumber); 802 } else { 803 // it's a real phone number, so strip out non-digits and look it up 804 final String strippedNumber = PhoneNumberUtils.stripSeparators(c.mNumber); 805 return getContactInfoForPhoneNumber(strippedNumber); 806 } 807 } 808 809 // Some received sms's have addresses such as "OakfieldCPS" or "T-Mobile". This 810 // function will attempt to identify these and return true. If the number contains 811 // 3 or more digits, such as "jello123", this function will return false. 812 // Some countries have 3 digits shortcodes and we have to identify them as numbers. 813 // http://en.wikipedia.org/wiki/Short_code 814 // Examples of input/output for this function: 815 // "Jello123" -> false [3 digits, it is considered to be the phone number "123"] 816 // "T-Mobile" -> true [it is considered to be the address "T-Mobile"] 817 // "Mobile1" -> true [1 digit, it is considered to be the address "Mobile1"] 818 // "Dogs77" -> true [2 digits, it is considered to be the address "Dogs77"] 819 // "****1" -> true [1 digits, it is considered to be the address "****1"] 820 // "#4#5#6#" -> true [it is considered to be the address "#4#5#6#"] 821 // "AB12" -> true [2 digits, it is considered to be the address "AB12"] 822 // "12" -> true [2 digits, it is considered to be the address "12"] isAlphaNumber(String number)823 private boolean isAlphaNumber(String number) { 824 // TODO: PhoneNumberUtils.isWellFormedSmsAddress() only check if the number is a valid 825 // GSM SMS address. If the address contains a dialable char, it considers it a well 826 // formed SMS addr. CDMA doesn't work that way and has a different parser for SMS 827 // address (see CdmaSmsAddress.parse(String address)). We should definitely fix this!!! 828 if (!PhoneNumberUtils.isWellFormedSmsAddress(number)) { 829 // The example "T-Mobile" will exit here because there are no numbers. 830 return true; // we're not an sms address, consider it an alpha number 831 } 832 if (MessageUtils.isAlias(number)) { 833 return true; 834 } 835 number = PhoneNumberUtils.extractNetworkPortion(number); 836 if (TextUtils.isEmpty(number)) { 837 return true; // there are no digits whatsoever in the number 838 } 839 // At this point, anything like "Mobile1" or "Dogs77" will be stripped down to 840 // "1" and "77". "#4#5#6#" remains as "#4#5#6#" at this point. 841 return number.length() < 3; 842 } 843 844 /** 845 * Queries the caller id info with the phone number. 846 * @return a Contact containing the caller id info corresponding to the number. 847 */ getContactInfoForPhoneNumber(String number)848 private Contact getContactInfoForPhoneNumber(String number) { 849 Contact entry = new Contact(number); 850 entry.mContactMethodType = CONTACT_METHOD_TYPE_PHONE; 851 entry.mPeopleReferenceUri = Uri.fromParts("tel", number, null); 852 853 if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { 854 log("queryContactInfoByNumber: number=" + number); 855 } 856 857 String normalizedNumber = PhoneNumberUtils.normalizeNumber(number); 858 String minMatch = PhoneNumberUtils.toCallerIDMinMatch(normalizedNumber); 859 if (!TextUtils.isEmpty(normalizedNumber) && !TextUtils.isEmpty(minMatch)) { 860 String numberLen = String.valueOf(normalizedNumber.length()); 861 String numberE164 = PhoneNumberUtils.formatNumberToE164( 862 number, MmsApp.getApplication().getCurrentCountryIso()); 863 String selection; 864 String[] args; 865 if (TextUtils.isEmpty(numberE164)) { 866 selection = CALLER_ID_SELECTION_WITHOUT_E164; 867 args = new String[] {minMatch, numberLen, normalizedNumber, numberLen}; 868 } else { 869 selection = CALLER_ID_SELECTION; 870 args = new String[] { 871 minMatch, numberE164, numberLen, normalizedNumber, numberLen}; 872 } 873 874 Cursor cursor = mContext.getContentResolver().query( 875 PHONES_WITH_PRESENCE_URI, CALLER_ID_PROJECTION, selection, args, null); 876 if (cursor == null) { 877 Log.w(TAG, "queryContactInfoByNumber(" + number + ") returned NULL cursor!" 878 + " contact uri used " + PHONES_WITH_PRESENCE_URI); 879 return entry; 880 } 881 882 try { 883 if (cursor.moveToFirst()) { 884 fillPhoneTypeContact(entry, cursor); 885 } 886 } finally { 887 cursor.close(); 888 } 889 } 890 return entry; 891 } 892 893 /** 894 * @return a Contact containing the info for the profile. 895 */ getContactInfoForSelf()896 private Contact getContactInfoForSelf() { 897 Contact entry = new Contact(true); 898 entry.mContactMethodType = CONTACT_METHOD_TYPE_SELF; 899 900 if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { 901 log("getContactInfoForSelf"); 902 } 903 Cursor cursor = mContext.getContentResolver().query( 904 Profile.CONTENT_URI, SELF_PROJECTION, null, null, null); 905 if (cursor == null) { 906 Log.w(TAG, "getContactInfoForSelf() returned NULL cursor!" 907 + " contact uri used " + Profile.CONTENT_URI); 908 return entry; 909 } 910 911 try { 912 if (cursor.moveToFirst()) { 913 fillSelfContact(entry, cursor); 914 } 915 } finally { 916 cursor.close(); 917 } 918 return entry; 919 } 920 fillPhoneTypeContact(final Contact contact, final Cursor cursor)921 private void fillPhoneTypeContact(final Contact contact, final Cursor cursor) { 922 synchronized (contact) { 923 contact.mContactMethodType = CONTACT_METHOD_TYPE_PHONE; 924 contact.mContactMethodId = cursor.getLong(PHONE_ID_COLUMN); 925 contact.mLabel = cursor.getString(PHONE_LABEL_COLUMN); 926 contact.mName = cursor.getString(CONTACT_NAME_COLUMN); 927 contact.mPersonId = cursor.getLong(CONTACT_ID_COLUMN); 928 contact.mPresenceResId = getPresenceIconResourceId( 929 cursor.getInt(CONTACT_PRESENCE_COLUMN)); 930 contact.mPresenceText = cursor.getString(CONTACT_STATUS_COLUMN); 931 contact.mNumberE164 = cursor.getString(PHONE_NORMALIZED_NUMBER); 932 contact.mSendToVoicemail = cursor.getInt(SEND_TO_VOICEMAIL) == 1; 933 if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { 934 log("fillPhoneTypeContact: name=" + contact.mName + ", number=" 935 + contact.mNumber + ", presence=" + contact.mPresenceResId 936 + " SendToVoicemail: " + contact.mSendToVoicemail); 937 } 938 } 939 byte[] data = loadAvatarData(contact); 940 941 synchronized (contact) { 942 contact.mAvatarData = data; 943 } 944 } 945 fillSelfContact(final Contact contact, final Cursor cursor)946 private void fillSelfContact(final Contact contact, final Cursor cursor) { 947 synchronized (contact) { 948 contact.mName = cursor.getString(SELF_NAME_COLUMN); 949 if (TextUtils.isEmpty(contact.mName)) { 950 contact.mName = mContext.getString(R.string.messagelist_sender_self); 951 } 952 if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { 953 log("fillSelfContact: name=" + contact.mName + ", number=" 954 + contact.mNumber); 955 } 956 } 957 byte[] data = loadAvatarData(contact); 958 959 synchronized (contact) { 960 contact.mAvatarData = data; 961 } 962 } 963 /* 964 * Load the avatar data from the cursor into memory. Don't decode the data 965 * until someone calls for it (see getAvatar). Hang onto the raw data so that 966 * we can compare it when the data is reloaded. 967 * TODO: consider comparing a checksum so that we don't have to hang onto 968 * the raw bytes after the image is decoded. 969 */ loadAvatarData(Contact entry)970 private byte[] loadAvatarData(Contact entry) { 971 byte [] data = null; 972 973 if ((!entry.mIsMe && entry.mPersonId == 0) || entry.mAvatar != null) { 974 return null; 975 } 976 977 if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { 978 log("loadAvatarData: name=" + entry.mName + ", number=" + entry.mNumber); 979 } 980 981 // If the contact is "me", then use my local profile photo. Otherwise, build a 982 // uri to get the avatar of the contact. 983 Uri contactUri = entry.mIsMe ? 984 Profile.CONTENT_URI : 985 ContentUris.withAppendedId(Contacts.CONTENT_URI, entry.mPersonId); 986 987 InputStream avatarDataStream = Contacts.openContactPhotoInputStream( 988 mContext.getContentResolver(), 989 contactUri); 990 try { 991 if (avatarDataStream != null) { 992 data = new byte[avatarDataStream.available()]; 993 avatarDataStream.read(data, 0, data.length); 994 } 995 } catch (IOException ex) { 996 // 997 } finally { 998 try { 999 if (avatarDataStream != null) { 1000 avatarDataStream.close(); 1001 } 1002 } catch (IOException e) { 1003 } 1004 } 1005 1006 return data; 1007 } 1008 getPresenceIconResourceId(int presence)1009 private int getPresenceIconResourceId(int presence) { 1010 // TODO: must fix for SDK 1011 if (presence != Presence.OFFLINE) { 1012 return Presence.getPresenceIconResourceId(presence); 1013 } 1014 1015 return 0; 1016 } 1017 1018 /** 1019 * Query the contact email table to get the name of an email address. 1020 */ getContactInfoForEmailAddress(String email)1021 private Contact getContactInfoForEmailAddress(String email) { 1022 Contact entry = new Contact(email); 1023 entry.mContactMethodType = CONTACT_METHOD_TYPE_EMAIL; 1024 entry.mPeopleReferenceUri = Uri.fromParts("mailto", email, null); 1025 1026 Cursor cursor = SqliteWrapper.query(mContext, mContext.getContentResolver(), 1027 EMAIL_WITH_PRESENCE_URI, 1028 EMAIL_PROJECTION, 1029 EMAIL_SELECTION, 1030 new String[] { email }, 1031 null); 1032 1033 if (cursor != null) { 1034 try { 1035 while (cursor.moveToNext()) { 1036 boolean found = false; 1037 synchronized (entry) { 1038 entry.mContactMethodId = cursor.getLong(EMAIL_ID_COLUMN); 1039 entry.mPresenceResId = getPresenceIconResourceId( 1040 cursor.getInt(EMAIL_STATUS_COLUMN)); 1041 entry.mPersonId = cursor.getLong(EMAIL_CONTACT_ID_COLUMN); 1042 entry.mSendToVoicemail = 1043 cursor.getInt(EMAIL_SEND_TO_VOICEMAIL_COLUMN) == 1; 1044 1045 String name = cursor.getString(EMAIL_NAME_COLUMN); 1046 if (TextUtils.isEmpty(name)) { 1047 name = cursor.getString(EMAIL_CONTACT_NAME_COLUMN); 1048 } 1049 if (!TextUtils.isEmpty(name)) { 1050 entry.mName = name; 1051 if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { 1052 log("getContactInfoForEmailAddress: name=" + entry.mName + 1053 ", email=" + email + ", presence=" + 1054 entry.mPresenceResId); 1055 } 1056 found = true; 1057 } 1058 } 1059 1060 if (found) { 1061 byte[] data = loadAvatarData(entry); 1062 synchronized (entry) { 1063 entry.mAvatarData = data; 1064 } 1065 1066 break; 1067 } 1068 } 1069 } finally { 1070 cursor.close(); 1071 } 1072 } 1073 return entry; 1074 } 1075 1076 // Invert and truncate to five characters the phoneNumber so that we 1077 // can use it as the key in a hashtable. We keep a mapping of this 1078 // key to a list of all contacts which have the same key. key(String phoneNumber, CharBuffer keyBuffer)1079 private String key(String phoneNumber, CharBuffer keyBuffer) { 1080 keyBuffer.clear(); 1081 keyBuffer.mark(); 1082 1083 int position = phoneNumber.length(); 1084 int resultCount = 0; 1085 while (--position >= 0) { 1086 char c = phoneNumber.charAt(position); 1087 if (Character.isDigit(c)) { 1088 keyBuffer.put(c); 1089 if (++resultCount == STATIC_KEY_BUFFER_MAXIMUM_LENGTH) { 1090 break; 1091 } 1092 } 1093 } 1094 keyBuffer.reset(); 1095 if (resultCount > 0) { 1096 return keyBuffer.toString(); 1097 } else { 1098 // there were no usable digits in the input phoneNumber 1099 return phoneNumber; 1100 } 1101 } 1102 1103 // Reuse this so we don't have to allocate each time we go through this 1104 // "get" function. 1105 static final int STATIC_KEY_BUFFER_MAXIMUM_LENGTH = 5; 1106 static CharBuffer sStaticKeyBuffer = CharBuffer.allocate(STATIC_KEY_BUFFER_MAXIMUM_LENGTH); 1107 internalGet(String numberOrEmail, boolean isMe)1108 private Contact internalGet(String numberOrEmail, boolean isMe) { 1109 synchronized (ContactsCache.this) { 1110 // See if we can find "number" in the hashtable. 1111 // If so, just return the result. 1112 final boolean isNotRegularPhoneNumber = isMe || Mms.isEmailAddress(numberOrEmail) || 1113 MessageUtils.isAlias(numberOrEmail); 1114 final String key = isNotRegularPhoneNumber ? 1115 numberOrEmail : key(numberOrEmail, sStaticKeyBuffer); 1116 1117 ArrayList<Contact> candidates = mContactsHash.get(key); 1118 if (candidates != null) { 1119 int length = candidates.size(); 1120 for (int i = 0; i < length; i++) { 1121 Contact c= candidates.get(i); 1122 if (isNotRegularPhoneNumber) { 1123 if (numberOrEmail.equals(c.mNumber)) { 1124 return c; 1125 } 1126 } else { 1127 if (PhoneNumberUtils.compare(numberOrEmail, c.mNumber)) { 1128 return c; 1129 } 1130 } 1131 } 1132 } else { 1133 candidates = new ArrayList<Contact>(); 1134 // call toString() since it may be the static CharBuffer 1135 mContactsHash.put(key, candidates); 1136 } 1137 Contact c = isMe ? 1138 new Contact(true) : 1139 new Contact(numberOrEmail); 1140 candidates.add(c); 1141 return c; 1142 } 1143 } 1144 invalidate()1145 void invalidate() { 1146 // Don't remove the contacts. Just mark them stale so we'll update their 1147 // info, particularly their presence. 1148 synchronized (ContactsCache.this) { 1149 for (ArrayList<Contact> alc : mContactsHash.values()) { 1150 for (Contact c : alc) { 1151 synchronized (c) { 1152 c.mIsStale = true; 1153 } 1154 } 1155 } 1156 } 1157 } 1158 1159 // Remove a contact from the ContactsCache based on the number or email address remove(Contact contact)1160 private void remove(Contact contact) { 1161 synchronized (ContactsCache.this) { 1162 String number = contact.getNumber(); 1163 final boolean isNotRegularPhoneNumber = contact.isMe() || 1164 Mms.isEmailAddress(number) || 1165 MessageUtils.isAlias(number); 1166 final String key = isNotRegularPhoneNumber ? 1167 number : key(number, sStaticKeyBuffer); 1168 ArrayList<Contact> candidates = mContactsHash.get(key); 1169 if (candidates != null) { 1170 int length = candidates.size(); 1171 for (int i = 0; i < length; i++) { 1172 Contact c = candidates.get(i); 1173 if (isNotRegularPhoneNumber) { 1174 if (number.equals(c.mNumber)) { 1175 candidates.remove(i); 1176 break; 1177 } 1178 } else { 1179 if (PhoneNumberUtils.compare(number, c.mNumber)) { 1180 candidates.remove(i); 1181 break; 1182 } 1183 } 1184 } 1185 if (candidates.size() == 0) { 1186 mContactsHash.remove(key); 1187 } 1188 } 1189 } 1190 } 1191 } 1192 log(String msg)1193 private static void log(String msg) { 1194 Log.d(TAG, msg); 1195 } 1196 } 1197