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