PageRenderTime 64ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

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

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