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