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 = "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 { 97 public void onUpdate(Contact updated); 98 } 99 100 private Contact(String number, String name) { 101 init(number, name); 102 } 103 /* 104 * Make a basic contact object with a phone number. 105 */ 106 private Contact(String number) { 107 init(number, ""); 108 } 109 110 private Contact(boolean isMe) { 111 init(SELF_ITEM_KEY, ""); 112 mIsMe = isMe; 113 } 114 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 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 137 public static void logWithTrace(String tag, 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 160 public static Contact get(String number, boolean canBlock) { 161 return sContactCache.get(number, canBlock); 162 } 163 164 public static Contact getMe(boolean canBlock) { 165 return sContactCache.getMe(canBlock); 166 } 167 168 public void removeFromCache() { 169 sContactCache.remove(this); 170 } 171 172 public static List<Contact> getByPhoneUris(Parcelable[] uris) { 173 return sContactCache.getContactInfoForPhoneUris(uris); 174 } 175 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 191 public boolean isMe() { 192 return mIsMe; 193 } 194 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 */ 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 (at) 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 226 public synchronized void reload() { 227 mIsStale = true; 228 sContactCache.get(mNumber, false); 229 } 230 231 public synchronized String getNumber() { 232 return mNumber; 233 } 234 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 246 public boolean isNumberModified() { 247 return mNumberIsModified; 248 } 249 250 public boolean getSendToVoicemail() { 251 return mSendToVoicemail; 252 } 253 254 public void setIsNumberModified(boolean flag) { 255 mNumberIsModified = flag; 256 } 257 258 public synchronized String getName() { 259 if (TextUtils.isEmpty(mName)) { 260 return mNumber; 261 } else { 262 return mName; 263 } 264 } 265 266 public synchronized String getNameAndNumber() { 267 return mNameAndNumber; 268 } 269 270 private void notSynchronizedUpdateNameAndNumber() { 271 mNameAndNumber = formatNameAndNumber(mName, mNumber, mNumberE164); 272 } 273 274 public synchronized long getRecipientId() { 275 return mRecipientId; 276 } 277 278 public synchronized void setRecipientId(long id) { 279 mRecipientId = id; 280 } 281 282 public synchronized String getLabel() { 283 return mLabel; 284 } 285 286 public synchronized Uri getUri() { 287 return ContentUris.withAppendedId(Contacts.CONTENT_URI, mPersonId); 288 } 289 290 public synchronized int getPresenceResId() { 291 return mPresenceResId; 292 } 293 294 public synchronized boolean existsInDatabase() { 295 return (mPersonId > 0); 296 } 297 298 public static void addListener(UpdateListener l) { 299 synchronized (mListeners) { 300 mListeners.add(l); 301 } 302 } 303 304 public static void removeListener(UpdateListener l) { 305 synchronized (mListeners) { 306 mListeners.remove(l); 307 } 308 } 309 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 320 public synchronized boolean isEmail() { 321 return Mms.isEmailAddress(mNumber); 322 } 323 324 public String getPresenceText() { 325 return mPresenceText; 326 } 327 328 public int getContactMethodType() { 329 return mContactMethodType; 330 } 331 332 public long getContactMethodId() { 333 return mContactMethodId; 334 } 335 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 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 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 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 474 private ContactsCache(Context context) { 475 mContext = context; 476 } 477 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 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 523 public void push(Runnable r) { 524 synchronized (mThingsToLoad) { 525 mThingsToLoad.add(r); 526 mThingsToLoad.notify(); 527 } 528 } 529 } 530 531 public void pushTask(Runnable r) { 532 mTaskQueue.push(r); 533 } 534 535 public Contact getMe(boolean canBlock) { 536 return get(SELF_ITEM_KEY, true, canBlock); 537 } 538 539 public Contact get(String number, boolean canBlock) { 540 return get(number, false, canBlock); 541 } 542 543 private Contact get(String number, boolean isMe, boolean canBlock) { 544 if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { 545 logWithTrace(TAG, "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 */ 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 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 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 */ 780 private Contact getContactInfo(Contact c) { 781 if (c.mIsMe) { 782 return getContactInfoForSelf(); 783 } else if (Mms.isEmailAddress(c.mNumber)) { 784 return getContactInfoForEmailAddress(c.mNumber); 785 } else if (isAlphaNumber(c.mNumber)) { 786 // first try to look it up in the email field 787 Contact contact = getContactInfoForEmailAddress(c.mNumber); 788 if (contact.existsInDatabase()) { 789 return contact; 790 } 791 // then look it up in the phone field 792 return getContactInfoForPhoneNumber(c.mNumber); 793 } else { 794 // it's a real phone number, so strip out non-digits and look it up 795 final String strippedNumber = PhoneNumberUtils.stripSeparators(c.mNumber); 796 return getContactInfoForPhoneNumber(strippedNumber); 797 } 798 } 799 800 // Some received sms's have addresses such as "OakfieldCPS" or "T-Mobile". This 801 // function will attempt to identify these and return true. If the number contains 802 // 3 or more digits, such as "jello123", this function will return false. 803 // Some countries have 3 digits shortcodes and we have to identify them as numbers. 804 // http://en.wikipedia.org/wiki/Short_code 805 // Examples of input/output for this function: 806 // "Jello123" -> false [3 digits, it is considered to be the phone number "123"] 807 // "T-Mobile" -> true [it is considered to be the address "T-Mobile"] 808 // "Mobile1" -> true [1 digit, it is considered to be the address "Mobile1"] 809 // "Dogs77" -> true [2 digits, it is considered to be the address "Dogs77"] 810 // "****1" -> true [1 digits, it is considered to be the address "****1"] 811 // "#4#5#6#" -> true [it is considered to be the address "#4#5#6#"] 812 // "AB12" -> true [2 digits, it is considered to be the address "AB12"] 813 // "12" -> true [2 digits, it is considered to be the address "12"] 814 private boolean isAlphaNumber(String number) { 815 // TODO: PhoneNumberUtils.isWellFormedSmsAddress() only check if the number is a valid 816 // GSM SMS address. If the address contains a dialable char, it considers it a well 817 // formed SMS addr. CDMA doesn't work that way and has a different parser for SMS 818 // address (see CdmaSmsAddress.parse(String address)). We should definitely fix this!!! 819 if (!PhoneNumberUtils.isWellFormedSmsAddress(number)) { 820 // The example "T-Mobile" will exit here because there are no numbers. 821 return true; // we're not an sms address, consider it an alpha number 822 } 823 if (MessageUtils.isAlias(number)) { 824 return true; 825 } 826 number = PhoneNumberUtils.extractNetworkPortion(number); 827 if (TextUtils.isEmpty(number)) { 828 return true; // there are no digits whatsoever in the number 829 } 830 // At this point, anything like "Mobile1" or "Dogs77" will be stripped down to 831 // "1" and "77". "#4#5#6#" remains as "#4#5#6#" at this point. 832 return number.length() < 3; 833 } 834 835 /** 836 * Queries the caller id info with the phone number. 837 * @return a Contact containing the caller id info corresponding to the number. 838 */ 839 private Contact getContactInfoForPhoneNumber(String number) { 840 Contact entry = new Contact(number); 841 entry.mContactMethodType = CONTACT_METHOD_TYPE_PHONE; 842 843 if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { 844 log("queryContactInfoByNumber: number=" + number); 845 } 846 847 String normalizedNumber = PhoneNumberUtils.normalizeNumber(number); 848 String minMatch = PhoneNumberUtils.toCallerIDMinMatch(normalizedNumber); 849 if (!TextUtils.isEmpty(normalizedNumber) && !TextUtils.isEmpty(minMatch)) { 850 String numberLen = String.valueOf(normalizedNumber.length()); 851 String numberE164 = PhoneNumberUtils.formatNumberToE164( 852 number, MmsApp.getApplication().getCurrentCountryIso()); 853 String selection; 854 String[] args; 855 if (TextUtils.isEmpty(numberE164)) { 856 selection = CALLER_ID_SELECTION_WITHOUT_E164; 857 args = new String[] {minMatch, numberLen, normalizedNumber, numberLen}; 858 } else { 859 selection = CALLER_ID_SELECTION; 860 args = new String[] { 861 minMatch, numberE164, numberLen, normalizedNumber, numberLen}; 862 } 863 864 Cursor cursor = mContext.getContentResolver().query( 865 PHONES_WITH_PRESENCE_URI, CALLER_ID_PROJECTION, selection, args, null); 866 if (cursor == null) { 867 Log.w(TAG, "queryContactInfoByNumber(" + number + ") returned NULL cursor!" 868 + " contact uri used " + PHONES_WITH_PRESENCE_URI); 869 return entry; 870 } 871 872 try { 873 if (cursor.moveToFirst()) { 874 fillPhoneTypeContact(entry, cursor); 875 } 876 } finally { 877 cursor.close(); 878 } 879 } 880 return entry; 881 } 882 883 /** 884 * @return a Contact containing the info for the profile. 885 */ 886 private Contact getContactInfoForSelf() { 887 Contact entry = new Contact(true); 888 entry.mContactMethodType = CONTACT_METHOD_TYPE_SELF; 889 890 if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { 891 log("getContactInfoForSelf"); 892 } 893 Cursor cursor = mContext.getContentResolver().query( 894 Profile.CONTENT_URI, SELF_PROJECTION, null, null, null); 895 if (cursor == null) { 896 Log.w(TAG, "getContactInfoForSelf() returned NULL cursor!" 897 + " contact uri used " + Profile.CONTENT_URI); 898 return entry; 899 } 900 901 try { 902 if (cursor.moveToFirst()) { 903 fillSelfContact(entry, cursor); 904 } 905 } finally { 906 cursor.close(); 907 } 908 return entry; 909 } 910 911 private void fillPhoneTypeContact(final Contact contact, final Cursor cursor) { 912 synchronized (contact) { 913 contact.mContactMethodType = CONTACT_METHOD_TYPE_PHONE; 914 contact.mContactMethodId = cursor.getLong(PHONE_ID_COLUMN); 915 contact.mLabel = cursor.getString(PHONE_LABEL_COLUMN); 916 contact.mName = cursor.getString(CONTACT_NAME_COLUMN); 917 contact.mPersonId = cursor.getLong(CONTACT_ID_COLUMN); 918 contact.mPresenceResId = getPresenceIconResourceId( 919 cursor.getInt(CONTACT_PRESENCE_COLUMN)); 920 contact.mPresenceText = cursor.getString(CONTACT_STATUS_COLUMN); 921 contact.mNumberE164 = cursor.getString(PHONE_NORMALIZED_NUMBER); 922 contact.mSendToVoicemail = cursor.getInt(SEND_TO_VOICEMAIL) == 1; 923 if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { 924 log("fillPhoneTypeContact: name=" + contact.mName + ", number=" 925 + contact.mNumber + ", presence=" + contact.mPresenceResId 926 + " SendToVoicemail: " + contact.mSendToVoicemail); 927 } 928 } 929 byte[] data = loadAvatarData(contact); 930 931 synchronized (contact) { 932 contact.mAvatarData = data; 933 } 934 } 935 936 private void fillSelfContact(final Contact contact, final Cursor cursor) { 937 synchronized (contact) { 938 contact.mName = cursor.getString(SELF_NAME_COLUMN); 939 if (TextUtils.isEmpty(contact.mName)) { 940 contact.mName = mContext.getString(R.string.messagelist_sender_self); 941 } 942 if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { 943 log("fillSelfContact: name=" + contact.mName + ", number=" 944 + contact.mNumber); 945 } 946 } 947 byte[] data = loadAvatarData(contact); 948 949 synchronized (contact) { 950 contact.mAvatarData = data; 951 } 952 } 953 /* 954 * Load the avatar data from the cursor into memory. Don't decode the data 955 * until someone calls for it (see getAvatar). Hang onto the raw data so that 956 * we can compare it when the data is reloaded. 957 * TODO: consider comparing a checksum so that we don't have to hang onto 958 * the raw bytes after the image is decoded. 959 */ 960 private byte[] loadAvatarData(Contact entry) { 961 byte [] data = null; 962 963 if ((!entry.mIsMe && entry.mPersonId == 0) || entry.mAvatar != null) { 964 return null; 965 } 966 967 if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { 968 log("loadAvatarData: name=" + entry.mName + ", number=" + entry.mNumber); 969 } 970 971 // If the contact is "me", then use my local profile photo. Otherwise, build a 972 // uri to get the avatar of the contact. 973 Uri contactUri = entry.mIsMe ? 974 Profile.CONTENT_URI : 975 ContentUris.withAppendedId(Contacts.CONTENT_URI, entry.mPersonId); 976 977 InputStream avatarDataStream = Contacts.openContactPhotoInputStream( 978 mContext.getContentResolver(), 979 contactUri); 980 try { 981 if (avatarDataStream != null) { 982 data = new byte[avatarDataStream.available()]; 983 avatarDataStream.read(data, 0, data.length); 984 } 985 } catch (IOException ex) { 986 // 987 } finally { 988 try { 989 if (avatarDataStream != null) { 990 avatarDataStream.close(); 991 } 992 } catch (IOException e) { 993 } 994 } 995 996 return data; 997 } 998 999 private int getPresenceIconResourceId(int presence) { 1000 // TODO: must fix for SDK 1001 if (presence != Presence.OFFLINE) { 1002 return Presence.getPresenceIconResourceId(presence); 1003 } 1004 1005 return 0; 1006 } 1007 1008 /** 1009 * Query the contact email table to get the name of an email address. 1010 */ 1011 private Contact getContactInfoForEmailAddress(String email) { 1012 Contact entry = new Contact(email); 1013 entry.mContactMethodType = CONTACT_METHOD_TYPE_EMAIL; 1014 1015 Cursor cursor = SqliteWrapper.query(mContext, mContext.getContentResolver(), 1016 EMAIL_WITH_PRESENCE_URI, 1017 EMAIL_PROJECTION, 1018 EMAIL_SELECTION, 1019 new String[] { email }, 1020 null); 1021 1022 if (cursor != null) { 1023 try { 1024 while (cursor.moveToNext()) { 1025 boolean found = false; 1026 synchronized (entry) { 1027 entry.mContactMethodId = cursor.getLong(EMAIL_ID_COLUMN); 1028 entry.mPresenceResId = getPresenceIconResourceId( 1029 cursor.getInt(EMAIL_STATUS_COLUMN)); 1030 entry.mPersonId = cursor.getLong(EMAIL_CONTACT_ID_COLUMN); 1031 entry.mSendToVoicemail = 1032 cursor.getInt(EMAIL_SEND_TO_VOICEMAIL_COLUMN) == 1; 1033 1034 String name = cursor.getString(EMAIL_NAME_COLUMN); 1035 if (TextUtils.isEmpty(name)) { 1036 name = cursor.getString(EMAIL_CONTACT_NAME_COLUMN); 1037 } 1038 if (!TextUtils.isEmpty(name)) { 1039 entry.mName = name; 1040 if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { 1041 log("getContactInfoForEmailAddress: name=" + entry.mName + 1042 ", email=" + email + ", presence=" + 1043 entry.mPresenceResId); 1044 } 1045 found = true; 1046 } 1047 } 1048 1049 if (found) { 1050 byte[] data = loadAvatarData(entry); 1051 synchronized (entry) { 1052 entry.mAvatarData = data; 1053 } 1054 1055 break; 1056 } 1057 } 1058 } finally { 1059 cursor.close(); 1060 } 1061 } 1062 return entry; 1063 } 1064 1065 // Invert and truncate to five characters the phoneNumber so that we 1066 // can use it as the key in a hashtable. We keep a mapping of this 1067 // key to a list of all contacts which have the same key. 1068 private String key(String phoneNumber, CharBuffer keyBuffer) { 1069 keyBuffer.clear(); 1070 keyBuffer.mark(); 1071 1072 int position = phoneNumber.length(); 1073 int resultCount = 0; 1074 while (--position >= 0) { 1075 char c = phoneNumber.charAt(position); 1076 if (Character.isDigit(c)) { 1077 keyBuffer.put(c); 1078 if (++resultCount == STATIC_KEY_BUFFER_MAXIMUM_LENGTH) { 1079 break; 1080 } 1081 } 1082 } 1083 keyBuffer.reset(); 1084 if (resultCount > 0) { 1085 return keyBuffer.toString(); 1086 } else { 1087 // there were no usable digits in the input phoneNumber 1088 return phoneNumber; 1089 } 1090 } 1091 1092 // Reuse this so we don't have to allocate each time we go through this 1093 // "get" function. 1094 static final int STATIC_KEY_BUFFER_MAXIMUM_LENGTH = 5; 1095 static CharBuffer sStaticKeyBuffer = CharBuffer.allocate(STATIC_KEY_BUFFER_MAXIMUM_LENGTH); 1096 1097 private Contact internalGet(String numberOrEmail, boolean isMe) { 1098 synchronized (ContactsCache.this) { 1099 // See if we can find "number" in the hashtable. 1100 // If so, just return the result. 1101 final boolean isNotRegularPhoneNumber = isMe || Mms.isEmailAddress(numberOrEmail) || 1102 MessageUtils.isAlias(numberOrEmail); 1103 final String key = isNotRegularPhoneNumber ? 1104 numberOrEmail : key(numberOrEmail, sStaticKeyBuffer); 1105 1106 ArrayList<Contact> candidates = mContactsHash.get(key); 1107 if (candidates != null) { 1108 int length = candidates.size(); 1109 for (int i = 0; i < length; i++) { 1110 Contact c= candidates.get(i); 1111 if (isNotRegularPhoneNumber) { 1112 if (numberOrEmail.equals(c.mNumber)) { 1113 return c; 1114 } 1115 } else { 1116 if (PhoneNumberUtils.compare(numberOrEmail, c.mNumber)) { 1117 return c; 1118 } 1119 } 1120 } 1121 } else { 1122 candidates = new ArrayList<Contact>(); 1123 // call toString() since it may be the static CharBuffer 1124 mContactsHash.put(key, candidates); 1125 } 1126 Contact c = isMe ? 1127 new Contact(true) : 1128 new Contact(numberOrEmail); 1129 candidates.add(c); 1130 return c; 1131 } 1132 } 1133 1134 void invalidate() { 1135 // Don't remove the contacts. Just mark them stale so we'll update their 1136 // info, particularly their presence. 1137 synchronized (ContactsCache.this) { 1138 for (ArrayList<Contact> alc : mContactsHash.values()) { 1139 for (Contact c : alc) { 1140 synchronized (c) { 1141 c.mIsStale = true; 1142 } 1143 } 1144 } 1145 } 1146 } 1147 1148 // Remove a contact from the ContactsCache based on the number or email address 1149 private void remove(Contact contact) { 1150 synchronized (ContactsCache.this) { 1151 String number = contact.getNumber(); 1152 final boolean isNotRegularPhoneNumber = contact.isMe() || 1153 Mms.isEmailAddress(number) || 1154 MessageUtils.isAlias(number); 1155 final String key = isNotRegularPhoneNumber ? 1156 number : key(number, sStaticKeyBuffer); 1157 ArrayList<Contact> candidates = mContactsHash.get(key); 1158 if (candidates != null) { 1159 int length = candidates.size(); 1160 for (int i = 0; i < length; i++) { 1161 Contact c = candidates.get(i); 1162 if (isNotRegularPhoneNumber) { 1163 if (number.equals(c.mNumber)) { 1164 candidates.remove(i); 1165 break; 1166 } 1167 } else { 1168 if (PhoneNumberUtils.compare(number, c.mNumber)) { 1169 candidates.remove(i); 1170 break; 1171 } 1172 } 1173 } 1174 if (candidates.size() == 0) { 1175 mContactsHash.remove(key); 1176 } 1177 } 1178 } 1179 } 1180 } 1181 1182 private static void log(String msg) { 1183 Log.d(TAG, msg); 1184 } 1185 } 1186