PageRenderTime 53ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/src/com/android/mms/data/Contact.java

https://github.com/MIPS/packages-apps-Mms
Java | 804 lines | 598 code | 115 blank | 91 comment | 100 complexity | d44786a54d8c4f058f88fb49eb8bed08 MD5 | raw file
  1. package com.android.mms.data;
  2. import java.io.IOException;
  3. import java.io.InputStream;
  4. import java.nio.CharBuffer;
  5. import java.util.ArrayList;
  6. import java.util.Arrays;
  7. import java.util.HashMap;
  8. import java.util.HashSet;
  9. import java.util.List;
  10. import android.content.ContentUris;
  11. import android.content.Context;
  12. import android.database.ContentObserver;
  13. import android.database.Cursor;
  14. import android.graphics.Bitmap;
  15. import android.graphics.BitmapFactory;
  16. import android.graphics.drawable.BitmapDrawable;
  17. import android.graphics.drawable.Drawable;
  18. import android.net.Uri;
  19. import android.os.Handler;
  20. import android.provider.ContactsContract.Contacts;
  21. import android.provider.ContactsContract.Data;
  22. import android.provider.ContactsContract.Presence;
  23. import android.provider.ContactsContract.CommonDataKinds.Email;
  24. import android.provider.ContactsContract.CommonDataKinds.Phone;
  25. import android.provider.Telephony.Mms;
  26. import android.telephony.PhoneNumberUtils;
  27. import android.text.TextUtils;
  28. import android.util.Log;
  29. import android.database.sqlite.SqliteWrapper;
  30. import com.android.mms.ui.MessageUtils;
  31. import com.android.mms.LogTag;
  32. public class Contact {
  33. private static final String TAG = "Contact";
  34. private static final boolean V = false;
  35. private static ContactsCache sContactCache;
  36. // private static final ContentObserver sContactsObserver = new ContentObserver(new Handler()) {
  37. // @Override
  38. // public void onChange(boolean selfUpdate) {
  39. // if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
  40. // log("contact changed, invalidate cache");
  41. // }
  42. // invalidateCache();
  43. // }
  44. // };
  45. private static final ContentObserver sPresenceObserver = new ContentObserver(new Handler()) {
  46. @Override
  47. public void onChange(boolean selfUpdate) {
  48. if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
  49. log("presence changed, invalidate cache");
  50. }
  51. invalidateCache();
  52. }
  53. };
  54. private final static HashSet<UpdateListener> mListeners = new HashSet<UpdateListener>();
  55. private String mNumber;
  56. private String mName;
  57. private String mNameAndNumber; // for display, e.g. Fred Flintstone <670-782-1123>
  58. private boolean mNumberIsModified; // true if the number is modified
  59. private long mRecipientId; // used to find the Recipient cache entry
  60. private String mLabel;
  61. private long mPersonId;
  62. private int mPresenceResId; // TODO: make this a state instead of a res ID
  63. private String mPresenceText;
  64. private BitmapDrawable mAvatar;
  65. private byte [] mAvatarData;
  66. private boolean mIsStale;
  67. private boolean mQueryPending;
  68. public interface UpdateListener {
  69. public void onUpdate(Contact updated);
  70. }
  71. /*
  72. * Make a basic contact object with a phone number.
  73. */
  74. private Contact(String number) {
  75. mName = "";
  76. setNumber(number);
  77. mNumberIsModified = false;
  78. mLabel = "";
  79. mPersonId = 0;
  80. mPresenceResId = 0;
  81. mIsStale = true;
  82. }
  83. @Override
  84. public String toString() {
  85. return String.format("{ number=%s, name=%s, nameAndNumber=%s, label=%s, person_id=%d, hash=%d }",
  86. (mNumber != null ? mNumber : "null"),
  87. (mName != null ? mName : "null"),
  88. (mNameAndNumber != null ? mNameAndNumber : "null"),
  89. (mLabel != null ? mLabel : "null"),
  90. mPersonId, hashCode());
  91. }
  92. private static void logWithTrace(String msg, Object... format) {
  93. Thread current = Thread.currentThread();
  94. StackTraceElement[] stack = current.getStackTrace();
  95. StringBuilder sb = new StringBuilder();
  96. sb.append("[");
  97. sb.append(current.getId());
  98. sb.append("] ");
  99. sb.append(String.format(msg, format));
  100. sb.append(" <- ");
  101. int stop = stack.length > 7 ? 7 : stack.length;
  102. for (int i = 3; i < stop; i++) {
  103. String methodName = stack[i].getMethodName();
  104. sb.append(methodName);
  105. if ((i+1) != stop) {
  106. sb.append(" <- ");
  107. }
  108. }
  109. Log.d(TAG, sb.toString());
  110. }
  111. public static Contact get(String number, boolean canBlock) {
  112. return sContactCache.get(number, canBlock);
  113. }
  114. public static void invalidateCache() {
  115. if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
  116. log("invalidateCache");
  117. }
  118. // While invalidating our local Cache doesn't remove the contacts, it will mark them
  119. // stale so the next time we're asked for a particular contact, we'll return that
  120. // stale contact and at the same time, fire off an asyncUpdateContact to update
  121. // that contact's info in the background. UI elements using the contact typically
  122. // call addListener() so they immediately get notified when the contact has been
  123. // updated with the latest info. They redraw themselves when we call the
  124. // listener's onUpdate().
  125. sContactCache.invalidate();
  126. }
  127. private static String emptyIfNull(String s) {
  128. return (s != null ? s : "");
  129. }
  130. public static String formatNameAndNumber(String name, String number) {
  131. // Format like this: Mike Cleron <(650) 555-1234>
  132. // Erick Tseng <(650) 555-1212>
  133. // Tutankhamun <tutank1341@gmail.com>
  134. // (408) 555-1289
  135. String formattedNumber = number;
  136. if (!Mms.isEmailAddress(number)) {
  137. formattedNumber = PhoneNumberUtils.formatNumber(number);
  138. }
  139. if (!TextUtils.isEmpty(name) && !name.equals(number)) {
  140. return name + " <" + formattedNumber + ">";
  141. } else {
  142. return formattedNumber;
  143. }
  144. }
  145. public synchronized void reload() {
  146. mIsStale = true;
  147. sContactCache.get(mNumber, false);
  148. }
  149. public synchronized String getNumber() {
  150. return mNumber;
  151. }
  152. public synchronized void setNumber(String number) {
  153. mNumber = number;
  154. notSynchronizedUpdateNameAndNumber();
  155. mNumberIsModified = true;
  156. }
  157. public boolean isNumberModified() {
  158. return mNumberIsModified;
  159. }
  160. public void setIsNumberModified(boolean flag) {
  161. mNumberIsModified = flag;
  162. }
  163. public synchronized String getName() {
  164. if (TextUtils.isEmpty(mName)) {
  165. return mNumber;
  166. } else {
  167. return mName;
  168. }
  169. }
  170. public synchronized String getNameAndNumber() {
  171. return mNameAndNumber;
  172. }
  173. private synchronized void updateNameAndNumber() {
  174. notSynchronizedUpdateNameAndNumber();
  175. }
  176. private void notSynchronizedUpdateNameAndNumber() {
  177. mNameAndNumber = formatNameAndNumber(mName, mNumber);
  178. }
  179. public synchronized long getRecipientId() {
  180. return mRecipientId;
  181. }
  182. public synchronized void setRecipientId(long id) {
  183. mRecipientId = id;
  184. }
  185. public synchronized String getLabel() {
  186. return mLabel;
  187. }
  188. public synchronized Uri getUri() {
  189. return ContentUris.withAppendedId(Contacts.CONTENT_URI, mPersonId);
  190. }
  191. public synchronized int getPresenceResId() {
  192. return mPresenceResId;
  193. }
  194. public synchronized boolean existsInDatabase() {
  195. return (mPersonId > 0);
  196. }
  197. public static void addListener(UpdateListener l) {
  198. synchronized (mListeners) {
  199. mListeners.add(l);
  200. }
  201. }
  202. public static void removeListener(UpdateListener l) {
  203. synchronized (mListeners) {
  204. mListeners.remove(l);
  205. }
  206. }
  207. public static synchronized void dumpListeners() {
  208. int i = 0;
  209. Log.i(TAG, "[Contact] dumpListeners; size=" + mListeners.size());
  210. for (UpdateListener listener : mListeners) {
  211. Log.i(TAG, "["+ (i++) + "]" + listener);
  212. }
  213. }
  214. public synchronized boolean isEmail() {
  215. return Mms.isEmailAddress(mNumber);
  216. }
  217. public String getPresenceText() {
  218. return mPresenceText;
  219. }
  220. public synchronized Drawable getAvatar(Context context, Drawable defaultValue) {
  221. if (mAvatar == null) {
  222. if (mAvatarData != null) {
  223. Bitmap b = BitmapFactory.decodeByteArray(mAvatarData, 0, mAvatarData.length);
  224. mAvatar = new BitmapDrawable(context.getResources(), b);
  225. }
  226. }
  227. return mAvatar != null ? mAvatar : defaultValue;
  228. }
  229. public static void init(final Context context) {
  230. sContactCache = new ContactsCache(context);
  231. RecipientIdCache.init(context);
  232. // it maybe too aggressive to listen for *any* contact changes, and rebuild MMS contact
  233. // cache each time that occurs. Unless we can get targeted updates for the contacts we
  234. // care about(which probably won't happen for a long time), we probably should just
  235. // invalidate cache peoridically, or surgically.
  236. /*
  237. context.getContentResolver().registerContentObserver(
  238. Contacts.CONTENT_URI, true, sContactsObserver);
  239. */
  240. }
  241. public static void dump() {
  242. sContactCache.dump();
  243. }
  244. private static class ContactsCache {
  245. private final TaskStack mTaskQueue = new TaskStack();
  246. private static final String SEPARATOR = ";";
  247. // query params for caller id lookup
  248. private static final String CALLER_ID_SELECTION = "PHONE_NUMBERS_EQUAL(" + Phone.NUMBER
  249. + ",?) AND " + Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'"
  250. + " AND " + Data.RAW_CONTACT_ID + " IN "
  251. + "(SELECT raw_contact_id "
  252. + " FROM phone_lookup"
  253. + " WHERE normalized_number GLOB('+*'))";
  254. // Utilizing private API
  255. private static final Uri PHONES_WITH_PRESENCE_URI = Data.CONTENT_URI;
  256. private static final String[] CALLER_ID_PROJECTION = new String[] {
  257. Phone.NUMBER, // 0
  258. Phone.LABEL, // 1
  259. Phone.DISPLAY_NAME, // 2
  260. Phone.CONTACT_ID, // 3
  261. Phone.CONTACT_PRESENCE, // 4
  262. Phone.CONTACT_STATUS, // 5
  263. };
  264. private static final int PHONE_NUMBER_COLUMN = 0;
  265. private static final int PHONE_LABEL_COLUMN = 1;
  266. private static final int CONTACT_NAME_COLUMN = 2;
  267. private static final int CONTACT_ID_COLUMN = 3;
  268. private static final int CONTACT_PRESENCE_COLUMN = 4;
  269. private static final int CONTACT_STATUS_COLUMN = 5;
  270. // query params for contact lookup by email
  271. private static final Uri EMAIL_WITH_PRESENCE_URI = Data.CONTENT_URI;
  272. private static final String EMAIL_SELECTION = "UPPER(" + Email.DATA + ")=UPPER(?) AND "
  273. + Data.MIMETYPE + "='" + Email.CONTENT_ITEM_TYPE + "'";
  274. private static final String[] EMAIL_PROJECTION = new String[] {
  275. Email.DISPLAY_NAME, // 0
  276. Email.CONTACT_PRESENCE, // 1
  277. Email.CONTACT_ID, // 2
  278. Phone.DISPLAY_NAME, //
  279. };
  280. private static final int EMAIL_NAME_COLUMN = 0;
  281. private static final int EMAIL_STATUS_COLUMN = 1;
  282. private static final int EMAIL_ID_COLUMN = 2;
  283. private static final int EMAIL_CONTACT_NAME_COLUMN = 3;
  284. private final Context mContext;
  285. private final HashMap<String, ArrayList<Contact>> mContactsHash =
  286. new HashMap<String, ArrayList<Contact>>();
  287. private ContactsCache(Context context) {
  288. mContext = context;
  289. }
  290. void dump() {
  291. synchronized (ContactsCache.this) {
  292. Log.d(TAG, "**** Contact cache dump ****");
  293. for (String key : mContactsHash.keySet()) {
  294. ArrayList<Contact> alc = mContactsHash.get(key);
  295. for (Contact c : alc) {
  296. Log.d(TAG, key + " ==> " + c.toString());
  297. }
  298. }
  299. }
  300. }
  301. private static class TaskStack {
  302. Thread mWorkerThread;
  303. private final ArrayList<Runnable> mThingsToLoad;
  304. public TaskStack() {
  305. mThingsToLoad = new ArrayList<Runnable>();
  306. mWorkerThread = new Thread(new Runnable() {
  307. public void run() {
  308. while (true) {
  309. Runnable r = null;
  310. synchronized (mThingsToLoad) {
  311. if (mThingsToLoad.size() == 0) {
  312. try {
  313. mThingsToLoad.wait();
  314. } catch (InterruptedException ex) {
  315. // nothing to do
  316. }
  317. }
  318. if (mThingsToLoad.size() > 0) {
  319. r = mThingsToLoad.remove(0);
  320. }
  321. }
  322. if (r != null) {
  323. r.run();
  324. }
  325. }
  326. }
  327. });
  328. mWorkerThread.start();
  329. }
  330. public void push(Runnable r) {
  331. synchronized (mThingsToLoad) {
  332. mThingsToLoad.add(r);
  333. mThingsToLoad.notify();
  334. }
  335. }
  336. }
  337. public void pushTask(Runnable r) {
  338. mTaskQueue.push(r);
  339. }
  340. public Contact get(String number, boolean canBlock) {
  341. if (V) logWithTrace("get(%s, %s)", number, canBlock);
  342. if (TextUtils.isEmpty(number)) {
  343. number = ""; // In some places (such as Korea), it's possible to receive
  344. // a message without the sender's address. In this case,
  345. // all such anonymous messages will get added to the same
  346. // thread.
  347. }
  348. // Always return a Contact object, if if we don't have an actual contact
  349. // in the contacts db.
  350. Contact contact = get(number);
  351. Runnable r = null;
  352. synchronized (contact) {
  353. // If there's a query pending and we're willing to block then
  354. // wait here until the query completes.
  355. while (canBlock && contact.mQueryPending) {
  356. try {
  357. contact.wait();
  358. } catch (InterruptedException ex) {
  359. // try again by virtue of the loop unless mQueryPending is false
  360. }
  361. }
  362. // If we're stale and we haven't already kicked off a query then kick
  363. // it off here.
  364. if (contact.mIsStale && !contact.mQueryPending) {
  365. contact.mIsStale = false;
  366. if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
  367. log("async update for " + contact.toString() + " canBlock: " + canBlock +
  368. " isStale: " + contact.mIsStale);
  369. }
  370. final Contact c = contact;
  371. r = new Runnable() {
  372. public void run() {
  373. updateContact(c);
  374. }
  375. };
  376. // set this to true while we have the lock on contact since we will
  377. // either run the query directly (canBlock case) or push the query
  378. // onto the queue. In either case the mQueryPending will get set
  379. // to false via updateContact.
  380. contact.mQueryPending = true;
  381. }
  382. }
  383. // do this outside of the synchronized so we don't hold up any
  384. // subsequent calls to "get" on other threads
  385. if (r != null) {
  386. if (canBlock) {
  387. r.run();
  388. } else {
  389. pushTask(r);
  390. }
  391. }
  392. return contact;
  393. }
  394. private boolean contactChanged(Contact orig, Contact newContactData) {
  395. // The phone number should never change, so don't bother checking.
  396. // TODO: Maybe update it if it has gotten longer, i.e. 650-234-5678 -> +16502345678?
  397. String oldName = emptyIfNull(orig.mName);
  398. String newName = emptyIfNull(newContactData.mName);
  399. if (!oldName.equals(newName)) {
  400. if (V) Log.d(TAG, String.format("name changed: %s -> %s", oldName, newName));
  401. return true;
  402. }
  403. String oldLabel = emptyIfNull(orig.mLabel);
  404. String newLabel = emptyIfNull(newContactData.mLabel);
  405. if (!oldLabel.equals(newLabel)) {
  406. if (V) Log.d(TAG, String.format("label changed: %s -> %s", oldLabel, newLabel));
  407. return true;
  408. }
  409. if (orig.mPersonId != newContactData.mPersonId) {
  410. if (V) Log.d(TAG, "person id changed");
  411. return true;
  412. }
  413. if (orig.mPresenceResId != newContactData.mPresenceResId) {
  414. if (V) Log.d(TAG, "presence changed");
  415. return true;
  416. }
  417. if (!Arrays.equals(orig.mAvatarData, newContactData.mAvatarData)) {
  418. if (V) Log.d(TAG, "avatar changed");
  419. return true;
  420. }
  421. return false;
  422. }
  423. private void updateContact(final Contact c) {
  424. if (c == null) {
  425. return;
  426. }
  427. Contact entry = getContactInfo(c.mNumber);
  428. synchronized (c) {
  429. if (contactChanged(c, entry)) {
  430. if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
  431. log("updateContact: contact changed for " + entry.mName);
  432. }
  433. c.mNumber = entry.mNumber;
  434. c.mLabel = entry.mLabel;
  435. c.mPersonId = entry.mPersonId;
  436. c.mPresenceResId = entry.mPresenceResId;
  437. c.mPresenceText = entry.mPresenceText;
  438. c.mAvatarData = entry.mAvatarData;
  439. c.mAvatar = entry.mAvatar;
  440. // Check to see if this is the local ("me") number and update the name.
  441. if (MessageUtils.isLocalNumber(c.mNumber)) {
  442. c.mName = mContext.getString(com.android.mms.R.string.me);
  443. } else {
  444. c.mName = entry.mName;
  445. }
  446. c.notSynchronizedUpdateNameAndNumber();
  447. // clone the list of listeners in case the onUpdate call turns around and
  448. // modifies the list of listeners
  449. // access to mListeners is synchronized on ContactsCache
  450. HashSet<UpdateListener> iterator;
  451. synchronized (mListeners) {
  452. iterator = (HashSet<UpdateListener>)Contact.mListeners.clone();
  453. }
  454. for (UpdateListener l : iterator) {
  455. if (V) Log.d(TAG, "updating " + l);
  456. l.onUpdate(c);
  457. }
  458. }
  459. synchronized (c) {
  460. c.mQueryPending = false;
  461. c.notifyAll();
  462. }
  463. }
  464. }
  465. /**
  466. * Returns the caller info in Contact.
  467. */
  468. public Contact getContactInfo(String numberOrEmail) {
  469. if (Mms.isEmailAddress(numberOrEmail)) {
  470. return getContactInfoForEmailAddress(numberOrEmail);
  471. } else {
  472. return getContactInfoForPhoneNumber(numberOrEmail);
  473. }
  474. }
  475. /**
  476. * Queries the caller id info with the phone number.
  477. * @return a Contact containing the caller id info corresponding to the number.
  478. */
  479. private Contact getContactInfoForPhoneNumber(String number) {
  480. number = PhoneNumberUtils.stripSeparators(number);
  481. Contact entry = new Contact(number);
  482. //if (LOCAL_DEBUG) log("queryContactInfoByNumber: number=" + number);
  483. // We need to include the phone number in the selection string itself rather then
  484. // selection arguments, because SQLite needs to see the exact pattern of GLOB
  485. // to generate the correct query plan
  486. String selection = CALLER_ID_SELECTION.replace("+",
  487. PhoneNumberUtils.toCallerIDMinMatch(number));
  488. Cursor cursor = mContext.getContentResolver().query(
  489. PHONES_WITH_PRESENCE_URI,
  490. CALLER_ID_PROJECTION,
  491. selection,
  492. new String[] { number },
  493. null);
  494. if (cursor == null) {
  495. Log.w(TAG, "queryContactInfoByNumber(" + number + ") returned NULL cursor!" +
  496. " contact uri used " + PHONES_WITH_PRESENCE_URI);
  497. return entry;
  498. }
  499. try {
  500. if (cursor.moveToFirst()) {
  501. synchronized (entry) {
  502. entry.mLabel = cursor.getString(PHONE_LABEL_COLUMN);
  503. entry.mName = cursor.getString(CONTACT_NAME_COLUMN);
  504. entry.mPersonId = cursor.getLong(CONTACT_ID_COLUMN);
  505. entry.mPresenceResId = getPresenceIconResourceId(
  506. cursor.getInt(CONTACT_PRESENCE_COLUMN));
  507. entry.mPresenceText = cursor.getString(CONTACT_STATUS_COLUMN);
  508. if (V) {
  509. log("queryContactInfoByNumber: name=" + entry.mName +
  510. ", number=" + number + ", presence=" + entry.mPresenceResId);
  511. }
  512. }
  513. byte[] data = loadAvatarData(entry);
  514. synchronized (entry) {
  515. entry.mAvatarData = data;
  516. }
  517. }
  518. } finally {
  519. cursor.close();
  520. }
  521. return entry;
  522. }
  523. /*
  524. * Load the avatar data from the cursor into memory. Don't decode the data
  525. * until someone calls for it (see getAvatar). Hang onto the raw data so that
  526. * we can compare it when the data is reloaded.
  527. * TODO: consider comparing a checksum so that we don't have to hang onto
  528. * the raw bytes after the image is decoded.
  529. */
  530. private byte[] loadAvatarData(Contact entry) {
  531. byte [] data = null;
  532. if (entry.mPersonId == 0 || entry.mAvatar != null) {
  533. return null;
  534. }
  535. Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, entry.mPersonId);
  536. InputStream avatarDataStream = Contacts.openContactPhotoInputStream(
  537. mContext.getContentResolver(),
  538. contactUri);
  539. try {
  540. if (avatarDataStream != null) {
  541. data = new byte[avatarDataStream.available()];
  542. avatarDataStream.read(data, 0, data.length);
  543. }
  544. } catch (IOException ex) {
  545. //
  546. } finally {
  547. try {
  548. if (avatarDataStream != null) {
  549. avatarDataStream.close();
  550. }
  551. } catch (IOException e) {
  552. }
  553. }
  554. return data;
  555. }
  556. private int getPresenceIconResourceId(int presence) {
  557. // TODO: must fix for SDK
  558. if (presence != Presence.OFFLINE) {
  559. return Presence.getPresenceIconResourceId(presence);
  560. }
  561. return 0;
  562. }
  563. /**
  564. * Query the contact email table to get the name of an email address.
  565. */
  566. private Contact getContactInfoForEmailAddress(String email) {
  567. Contact entry = new Contact(email);
  568. Cursor cursor = SqliteWrapper.query(mContext, mContext.getContentResolver(),
  569. EMAIL_WITH_PRESENCE_URI,
  570. EMAIL_PROJECTION,
  571. EMAIL_SELECTION,
  572. new String[] { email },
  573. null);
  574. if (cursor != null) {
  575. try {
  576. while (cursor.moveToNext()) {
  577. boolean found = false;
  578. synchronized (entry) {
  579. entry.mPresenceResId = getPresenceIconResourceId(
  580. cursor.getInt(EMAIL_STATUS_COLUMN));
  581. entry.mPersonId = cursor.getLong(EMAIL_ID_COLUMN);
  582. String name = cursor.getString(EMAIL_NAME_COLUMN);
  583. if (TextUtils.isEmpty(name)) {
  584. name = cursor.getString(EMAIL_CONTACT_NAME_COLUMN);
  585. }
  586. if (!TextUtils.isEmpty(name)) {
  587. entry.mName = name;
  588. if (V) {
  589. log("getContactInfoForEmailAddress: name=" + entry.mName +
  590. ", email=" + email + ", presence=" +
  591. entry.mPresenceResId);
  592. }
  593. found = true;
  594. }
  595. }
  596. if (found) {
  597. byte[] data = loadAvatarData(entry);
  598. synchronized (entry) {
  599. entry.mAvatarData = data;
  600. }
  601. break;
  602. }
  603. }
  604. } finally {
  605. cursor.close();
  606. }
  607. }
  608. return entry;
  609. }
  610. // Invert and truncate to five characters the phoneNumber so that we
  611. // can use it as the key in a hashtable. We keep a mapping of this
  612. // key to a list of all contacts which have the same key.
  613. private String key(String phoneNumber, CharBuffer keyBuffer) {
  614. keyBuffer.clear();
  615. keyBuffer.mark();
  616. int position = phoneNumber.length();
  617. int resultCount = 0;
  618. while (--position >= 0) {
  619. char c = phoneNumber.charAt(position);
  620. if (Character.isDigit(c)) {
  621. keyBuffer.put(c);
  622. if (++resultCount == STATIC_KEY_BUFFER_MAXIMUM_LENGTH) {
  623. break;
  624. }
  625. }
  626. }
  627. keyBuffer.reset();
  628. if (resultCount > 0) {
  629. return keyBuffer.toString();
  630. } else {
  631. // there were no usable digits in the input phoneNumber
  632. return phoneNumber;
  633. }
  634. }
  635. // Reuse this so we don't have to allocate each time we go through this
  636. // "get" function.
  637. static final int STATIC_KEY_BUFFER_MAXIMUM_LENGTH = 5;
  638. static CharBuffer sStaticKeyBuffer = CharBuffer.allocate(STATIC_KEY_BUFFER_MAXIMUM_LENGTH);
  639. public Contact get(String numberOrEmail) {
  640. synchronized (ContactsCache.this) {
  641. // See if we can find "number" in the hashtable.
  642. // If so, just return the result.
  643. final boolean isNotRegularPhoneNumber = Mms.isEmailAddress(numberOrEmail) ||
  644. MessageUtils.isAlias(numberOrEmail);
  645. final String key = isNotRegularPhoneNumber ?
  646. numberOrEmail : key(numberOrEmail, sStaticKeyBuffer);
  647. ArrayList<Contact> candidates = mContactsHash.get(key);
  648. if (candidates != null) {
  649. int length = candidates.size();
  650. for (int i = 0; i < length; i++) {
  651. Contact c= candidates.get(i);
  652. if (isNotRegularPhoneNumber) {
  653. if (numberOrEmail.equals(c.mNumber)) {
  654. return c;
  655. }
  656. } else {
  657. if (PhoneNumberUtils.compare(numberOrEmail, c.mNumber)) {
  658. return c;
  659. }
  660. }
  661. }
  662. } else {
  663. candidates = new ArrayList<Contact>();
  664. // call toString() since it may be the static CharBuffer
  665. mContactsHash.put(key, candidates);
  666. }
  667. Contact c = new Contact(numberOrEmail);
  668. candidates.add(c);
  669. return c;
  670. }
  671. }
  672. void invalidate() {
  673. // Don't remove the contacts. Just mark them stale so we'll update their
  674. // info, particularly their presence.
  675. synchronized (ContactsCache.this) {
  676. for (ArrayList<Contact> alc : mContactsHash.values()) {
  677. for (Contact c : alc) {
  678. synchronized (c) {
  679. c.mIsStale = true;
  680. }
  681. }
  682. }
  683. }
  684. }
  685. }
  686. private static void log(String msg) {
  687. Log.d(TAG, msg);
  688. }
  689. }