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