| package com.android.mms.data; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.nio.CharBuffer; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| |
| import android.content.ContentUris; |
| import android.content.Context; |
| import android.database.ContentObserver; |
| import android.database.Cursor; |
| import android.database.sqlite.SqliteWrapper; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.graphics.drawable.BitmapDrawable; |
| import android.graphics.drawable.Drawable; |
| import android.net.Uri; |
| import android.os.Handler; |
| import android.os.Parcelable; |
| import android.provider.ContactsContract.CommonDataKinds.Email; |
| import android.provider.ContactsContract.CommonDataKinds.Phone; |
| import android.provider.ContactsContract.Contacts; |
| import android.provider.ContactsContract.Data; |
| import android.provider.ContactsContract.Presence; |
| import android.provider.ContactsContract.Profile; |
| import android.provider.Telephony.Mms; |
| import android.telephony.PhoneNumberUtils; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import com.android.mms.LogTag; |
| import com.android.mms.MmsApp; |
| import com.android.mms.R; |
| import com.android.mms.ui.MessageUtils; |
| |
| public class Contact { |
| public static final int CONTACT_METHOD_TYPE_UNKNOWN = 0; |
| public static final int CONTACT_METHOD_TYPE_PHONE = 1; |
| public static final int CONTACT_METHOD_TYPE_EMAIL = 2; |
| public static final int CONTACT_METHOD_TYPE_SELF = 3; // the "Me" or profile contact |
| public static final String TEL_SCHEME = "tel"; |
| public static final String CONTENT_SCHEME = "content"; |
| private static final int CONTACT_METHOD_ID_UNKNOWN = -1; |
| private static final String TAG = "Contact"; |
| private static ContactsCache sContactCache; |
| private static final String SELF_ITEM_KEY = "Self_Item_Key"; |
| |
| // private static final ContentObserver sContactsObserver = new ContentObserver(new Handler()) { |
| // @Override |
| // public void onChange(boolean selfUpdate) { |
| // if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { |
| // log("contact changed, invalidate cache"); |
| // } |
| // invalidateCache(); |
| // } |
| // }; |
| |
| private static final ContentObserver sPresenceObserver = new ContentObserver(new Handler()) { |
| @Override |
| public void onChange(boolean selfUpdate) { |
| if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { |
| log("presence changed, invalidate cache"); |
| } |
| invalidateCache(); |
| } |
| }; |
| |
| private final static HashSet<UpdateListener> mListeners = new HashSet<UpdateListener>(); |
| |
| private long mContactMethodId; // Id in phone or email Uri returned by provider of current |
| // Contact, -1 is invalid. e.g. contact method id is 20 when |
| // current contact has phone content://.../phones/20. |
| private int mContactMethodType; |
| private String mNumber; |
| private String mNumberE164; |
| private String mName; |
| private String mNameAndNumber; // for display, e.g. Fred Flintstone <670-782-1123> |
| private boolean mNumberIsModified; // true if the number is modified |
| |
| private long mRecipientId; // used to find the Recipient cache entry |
| private String mLabel; |
| private long mPersonId; |
| private int mPresenceResId; // TODO: make this a state instead of a res ID |
| private String mPresenceText; |
| private BitmapDrawable mAvatar; |
| private byte [] mAvatarData; |
| private boolean mIsStale; |
| private boolean mQueryPending; |
| private boolean mIsMe; // true if this contact is me! |
| private boolean mSendToVoicemail; // true if this contact should not put up notification |
| |
| public interface UpdateListener { |
| public void onUpdate(Contact updated); |
| } |
| |
| private Contact(String number, String name) { |
| init(number, name); |
| } |
| /* |
| * Make a basic contact object with a phone number. |
| */ |
| private Contact(String number) { |
| init(number, ""); |
| } |
| |
| private Contact(boolean isMe) { |
| init(SELF_ITEM_KEY, ""); |
| mIsMe = isMe; |
| } |
| |
| private void init(String number, String name) { |
| mContactMethodId = CONTACT_METHOD_ID_UNKNOWN; |
| mName = name; |
| setNumber(number); |
| mNumberIsModified = false; |
| mLabel = ""; |
| mPersonId = 0; |
| mPresenceResId = 0; |
| mIsStale = true; |
| mSendToVoicemail = false; |
| } |
| @Override |
| public String toString() { |
| return String.format("{ number=%s, name=%s, nameAndNumber=%s, label=%s, person_id=%d, hash=%d method_id=%d }", |
| (mNumber != null ? mNumber : "null"), |
| (mName != null ? mName : "null"), |
| (mNameAndNumber != null ? mNameAndNumber : "null"), |
| (mLabel != null ? mLabel : "null"), |
| mPersonId, hashCode(), |
| mContactMethodId); |
| } |
| |
| public static void logWithTrace(String tag, String msg, Object... format) { |
| Thread current = Thread.currentThread(); |
| StackTraceElement[] stack = current.getStackTrace(); |
| |
| StringBuilder sb = new StringBuilder(); |
| sb.append("["); |
| sb.append(current.getId()); |
| sb.append("] "); |
| sb.append(String.format(msg, format)); |
| |
| sb.append(" <- "); |
| int stop = stack.length > 7 ? 7 : stack.length; |
| for (int i = 3; i < stop; i++) { |
| String methodName = stack[i].getMethodName(); |
| sb.append(methodName); |
| if ((i+1) != stop) { |
| sb.append(" <- "); |
| } |
| } |
| |
| Log.d(tag, sb.toString()); |
| } |
| |
| public static Contact get(String number, boolean canBlock) { |
| return sContactCache.get(number, canBlock); |
| } |
| |
| public static Contact getMe(boolean canBlock) { |
| return sContactCache.getMe(canBlock); |
| } |
| |
| public void removeFromCache() { |
| sContactCache.remove(this); |
| } |
| |
| public static List<Contact> getByPhoneUris(Parcelable[] uris) { |
| return sContactCache.getContactInfoForPhoneUris(uris); |
| } |
| |
| public static void invalidateCache() { |
| if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { |
| log("invalidateCache"); |
| } |
| |
| // While invalidating our local Cache doesn't remove the contacts, it will mark them |
| // stale so the next time we're asked for a particular contact, we'll return that |
| // stale contact and at the same time, fire off an asyncUpdateContact to update |
| // that contact's info in the background. UI elements using the contact typically |
| // call addListener() so they immediately get notified when the contact has been |
| // updated with the latest info. They redraw themselves when we call the |
| // listener's onUpdate(). |
| sContactCache.invalidate(); |
| } |
| |
| public boolean isMe() { |
| return mIsMe; |
| } |
| |
| private static String emptyIfNull(String s) { |
| return (s != null ? s : ""); |
| } |
| |
| /** |
| * Fomat the name and number. |
| * |
| * @param name |
| * @param number |
| * @param numberE164 the number's E.164 representation, is used to get the |
| * country the number belongs to. |
| * @return the formatted name and number |
| */ |
| public static String formatNameAndNumber(String name, String number, String numberE164) { |
| // Format like this: Mike Cleron <(650) 555-1234> |
| // Erick Tseng <(650) 555-1212> |
| // Tutankhamun <tutank1341@gmail.com> |
| // (408) 555-1289 |
| String formattedNumber = number; |
| if (!Mms.isEmailAddress(number)) { |
| formattedNumber = PhoneNumberUtils.formatNumber(number, numberE164, |
| MmsApp.getApplication().getCurrentCountryIso()); |
| } |
| |
| if (!TextUtils.isEmpty(name) && !name.equals(number)) { |
| return name + " <" + formattedNumber + ">"; |
| } else { |
| return formattedNumber; |
| } |
| } |
| |
| public synchronized void reload() { |
| mIsStale = true; |
| sContactCache.get(mNumber, false); |
| } |
| |
| public synchronized String getNumber() { |
| return mNumber; |
| } |
| |
| public synchronized void setNumber(String number) { |
| if (!Mms.isEmailAddress(number)) { |
| mNumber = PhoneNumberUtils.formatNumber(number, mNumberE164, |
| MmsApp.getApplication().getCurrentCountryIso()); |
| } else { |
| mNumber = number; |
| } |
| notSynchronizedUpdateNameAndNumber(); |
| mNumberIsModified = true; |
| } |
| |
| public boolean isNumberModified() { |
| return mNumberIsModified; |
| } |
| |
| public boolean getSendToVoicemail() { |
| return mSendToVoicemail; |
| } |
| |
| public void setIsNumberModified(boolean flag) { |
| mNumberIsModified = flag; |
| } |
| |
| public synchronized String getName() { |
| if (TextUtils.isEmpty(mName)) { |
| return mNumber; |
| } else { |
| return mName; |
| } |
| } |
| |
| public synchronized String getNameAndNumber() { |
| return mNameAndNumber; |
| } |
| |
| private void notSynchronizedUpdateNameAndNumber() { |
| mNameAndNumber = formatNameAndNumber(mName, mNumber, mNumberE164); |
| } |
| |
| public synchronized long getRecipientId() { |
| return mRecipientId; |
| } |
| |
| public synchronized void setRecipientId(long id) { |
| mRecipientId = id; |
| } |
| |
| public synchronized String getLabel() { |
| return mLabel; |
| } |
| |
| public synchronized Uri getUri() { |
| return ContentUris.withAppendedId(Contacts.CONTENT_URI, mPersonId); |
| } |
| |
| public synchronized int getPresenceResId() { |
| return mPresenceResId; |
| } |
| |
| public synchronized boolean existsInDatabase() { |
| return (mPersonId > 0); |
| } |
| |
| public static void addListener(UpdateListener l) { |
| synchronized (mListeners) { |
| mListeners.add(l); |
| } |
| } |
| |
| public static void removeListener(UpdateListener l) { |
| synchronized (mListeners) { |
| mListeners.remove(l); |
| } |
| } |
| |
| public static void dumpListeners() { |
| synchronized (mListeners) { |
| int i = 0; |
| Log.i(TAG, "[Contact] dumpListeners; size=" + mListeners.size()); |
| for (UpdateListener listener : mListeners) { |
| Log.i(TAG, "["+ (i++) + "]" + listener); |
| } |
| } |
| } |
| |
| public synchronized boolean isEmail() { |
| return Mms.isEmailAddress(mNumber); |
| } |
| |
| public String getPresenceText() { |
| return mPresenceText; |
| } |
| |
| public int getContactMethodType() { |
| return mContactMethodType; |
| } |
| |
| public long getContactMethodId() { |
| return mContactMethodId; |
| } |
| |
| public synchronized Uri getPhoneUri() { |
| if (existsInDatabase()) { |
| return ContentUris.withAppendedId(Phone.CONTENT_URI, mContactMethodId); |
| } else { |
| Uri.Builder ub = new Uri.Builder(); |
| ub.scheme(TEL_SCHEME); |
| ub.encodedOpaquePart(mNumber); |
| return ub.build(); |
| } |
| } |
| |
| public synchronized Drawable getAvatar(Context context, Drawable defaultValue) { |
| if (mAvatar == null) { |
| if (mAvatarData != null) { |
| Bitmap b = BitmapFactory.decodeByteArray(mAvatarData, 0, mAvatarData.length); |
| mAvatar = new BitmapDrawable(context.getResources(), b); |
| } |
| } |
| return mAvatar != null ? mAvatar : defaultValue; |
| } |
| |
| public static void init(final Context context) { |
| sContactCache = new ContactsCache(context); |
| |
| RecipientIdCache.init(context); |
| |
| // it maybe too aggressive to listen for *any* contact changes, and rebuild MMS contact |
| // cache each time that occurs. Unless we can get targeted updates for the contacts we |
| // care about(which probably won't happen for a long time), we probably should just |
| // invalidate cache peoridically, or surgically. |
| /* |
| context.getContentResolver().registerContentObserver( |
| Contacts.CONTENT_URI, true, sContactsObserver); |
| */ |
| } |
| |
| public static void dump() { |
| sContactCache.dump(); |
| } |
| |
| private static class ContactsCache { |
| private final TaskStack mTaskQueue = new TaskStack(); |
| private static final String SEPARATOR = ";"; |
| |
| /** |
| * For a specified phone number, 2 rows were inserted into phone_lookup |
| * table. One is the phone number's E164 representation, and another is |
| * one's normalized format. If the phone number's normalized format in |
| * the lookup table is the suffix of the given number's one, it is |
| * treated as matched CallerId. E164 format number must fully equal. |
| * |
| * For example: Both 650-123-4567 and +1 (650) 123-4567 will match the |
| * normalized number 6501234567 in the phone lookup. |
| * |
| * The min_match is used to narrow down the candidates for the final |
| * comparison. |
| */ |
| // query params for caller id lookup |
| private static final String CALLER_ID_SELECTION = " Data._ID IN " |
| + " (SELECT DISTINCT lookup.data_id " |
| + " FROM " |
| + " (SELECT data_id, normalized_number, length(normalized_number) as len " |
| + " FROM phone_lookup " |
| + " WHERE min_match = ?) AS lookup " |
| + " WHERE lookup.normalized_number = ? OR" |
| + " (lookup.len <= ? AND " |
| + " substr(?, ? - lookup.len + 1) = lookup.normalized_number))"; |
| |
| // query params for caller id lookup without E164 number as param |
| private static final String CALLER_ID_SELECTION_WITHOUT_E164 = " Data._ID IN " |
| + " (SELECT DISTINCT lookup.data_id " |
| + " FROM " |
| + " (SELECT data_id, normalized_number, length(normalized_number) as len " |
| + " FROM phone_lookup " |
| + " WHERE min_match = ?) AS lookup " |
| + " WHERE " |
| + " (lookup.len <= ? AND " |
| + " substr(?, ? - lookup.len + 1) = lookup.normalized_number))"; |
| |
| // Utilizing private API |
| private static final Uri PHONES_WITH_PRESENCE_URI = Data.CONTENT_URI; |
| |
| private static final String[] CALLER_ID_PROJECTION = new String[] { |
| Phone._ID, // 0 |
| Phone.NUMBER, // 1 |
| Phone.LABEL, // 2 |
| Phone.DISPLAY_NAME, // 3 |
| Phone.CONTACT_ID, // 4 |
| Phone.CONTACT_PRESENCE, // 5 |
| Phone.CONTACT_STATUS, // 6 |
| Phone.NORMALIZED_NUMBER, // 7 |
| Contacts.SEND_TO_VOICEMAIL // 8 |
| }; |
| |
| private static final int PHONE_ID_COLUMN = 0; |
| private static final int PHONE_NUMBER_COLUMN = 1; |
| private static final int PHONE_LABEL_COLUMN = 2; |
| private static final int CONTACT_NAME_COLUMN = 3; |
| private static final int CONTACT_ID_COLUMN = 4; |
| private static final int CONTACT_PRESENCE_COLUMN = 5; |
| private static final int CONTACT_STATUS_COLUMN = 6; |
| private static final int PHONE_NORMALIZED_NUMBER = 7; |
| private static final int SEND_TO_VOICEMAIL = 8; |
| |
| private static final String[] SELF_PROJECTION = new String[] { |
| Phone._ID, // 0 |
| Phone.DISPLAY_NAME, // 1 |
| }; |
| |
| private static final int SELF_ID_COLUMN = 0; |
| private static final int SELF_NAME_COLUMN = 1; |
| |
| // query params for contact lookup by email |
| private static final Uri EMAIL_WITH_PRESENCE_URI = Data.CONTENT_URI; |
| |
| private static final String EMAIL_SELECTION = "UPPER(" + Email.DATA + ")=UPPER(?) AND " |
| + Data.MIMETYPE + "='" + Email.CONTENT_ITEM_TYPE + "'"; |
| |
| private static final String[] EMAIL_PROJECTION = new String[] { |
| Email._ID, // 0 |
| Email.DISPLAY_NAME, // 1 |
| Email.CONTACT_PRESENCE, // 2 |
| Email.CONTACT_ID, // 3 |
| Phone.DISPLAY_NAME, // 4 |
| Contacts.SEND_TO_VOICEMAIL // 5 |
| }; |
| private static final int EMAIL_ID_COLUMN = 0; |
| private static final int EMAIL_NAME_COLUMN = 1; |
| private static final int EMAIL_STATUS_COLUMN = 2; |
| private static final int EMAIL_CONTACT_ID_COLUMN = 3; |
| private static final int EMAIL_CONTACT_NAME_COLUMN = 4; |
| private static final int EMAIL_SEND_TO_VOICEMAIL_COLUMN = 5; |
| |
| private final Context mContext; |
| |
| private final HashMap<String, ArrayList<Contact>> mContactsHash = |
| new HashMap<String, ArrayList<Contact>>(); |
| |
| private ContactsCache(Context context) { |
| mContext = context; |
| } |
| |
| void dump() { |
| synchronized (ContactsCache.this) { |
| Log.d(TAG, "**** Contact cache dump ****"); |
| for (String key : mContactsHash.keySet()) { |
| ArrayList<Contact> alc = mContactsHash.get(key); |
| for (Contact c : alc) { |
| Log.d(TAG, key + " ==> " + c.toString()); |
| } |
| } |
| } |
| } |
| |
| private static class TaskStack { |
| Thread mWorkerThread; |
| private final ArrayList<Runnable> mThingsToLoad; |
| |
| public TaskStack() { |
| mThingsToLoad = new ArrayList<Runnable>(); |
| mWorkerThread = new Thread(new Runnable() { |
| @Override |
| public void run() { |
| while (true) { |
| Runnable r = null; |
| synchronized (mThingsToLoad) { |
| if (mThingsToLoad.size() == 0) { |
| try { |
| mThingsToLoad.wait(); |
| } catch (InterruptedException ex) { |
| // nothing to do |
| } |
| } |
| if (mThingsToLoad.size() > 0) { |
| r = mThingsToLoad.remove(0); |
| } |
| } |
| if (r != null) { |
| r.run(); |
| } |
| } |
| } |
| }, "Contact.ContactsCache.TaskStack worker thread"); |
| mWorkerThread.setPriority(Thread.MIN_PRIORITY); |
| mWorkerThread.start(); |
| } |
| |
| public void push(Runnable r) { |
| synchronized (mThingsToLoad) { |
| mThingsToLoad.add(r); |
| mThingsToLoad.notify(); |
| } |
| } |
| } |
| |
| public void pushTask(Runnable r) { |
| mTaskQueue.push(r); |
| } |
| |
| public Contact getMe(boolean canBlock) { |
| return get(SELF_ITEM_KEY, true, canBlock); |
| } |
| |
| public Contact get(String number, boolean canBlock) { |
| return get(number, false, canBlock); |
| } |
| |
| private Contact get(String number, boolean isMe, boolean canBlock) { |
| if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { |
| logWithTrace(TAG, "get(%s, %s, %s)", number, isMe, canBlock); |
| } |
| |
| if (TextUtils.isEmpty(number)) { |
| number = ""; // In some places (such as Korea), it's possible to receive |
| // a message without the sender's address. In this case, |
| // all such anonymous messages will get added to the same |
| // thread. |
| } |
| |
| // Always return a Contact object, if if we don't have an actual contact |
| // in the contacts db. |
| Contact contact = internalGet(number, isMe); |
| Runnable r = null; |
| |
| synchronized (contact) { |
| // If there's a query pending and we're willing to block then |
| // wait here until the query completes. |
| while (canBlock && contact.mQueryPending) { |
| try { |
| contact.wait(); |
| } catch (InterruptedException ex) { |
| // try again by virtue of the loop unless mQueryPending is false |
| } |
| } |
| |
| // If we're stale and we haven't already kicked off a query then kick |
| // it off here. |
| if (contact.mIsStale && !contact.mQueryPending) { |
| contact.mIsStale = false; |
| |
| if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { |
| log("async update for " + contact.toString() + " canBlock: " + canBlock + |
| " isStale: " + contact.mIsStale); |
| } |
| |
| final Contact c = contact; |
| r = new Runnable() { |
| @Override |
| public void run() { |
| updateContact(c); |
| } |
| }; |
| |
| // set this to true while we have the lock on contact since we will |
| // either run the query directly (canBlock case) or push the query |
| // onto the queue. In either case the mQueryPending will get set |
| // to false via updateContact. |
| contact.mQueryPending = true; |
| } |
| } |
| // do this outside of the synchronized so we don't hold up any |
| // subsequent calls to "get" on other threads |
| if (r != null) { |
| if (canBlock) { |
| r.run(); |
| } else { |
| pushTask(r); |
| } |
| } |
| return contact; |
| } |
| |
| /** |
| * Get CacheEntry list for given phone URIs. This method will do single one query to |
| * get expected contacts from provider. Be sure passed in URIs are not null and contains |
| * only valid URIs. |
| */ |
| public List<Contact> getContactInfoForPhoneUris(Parcelable[] uris) { |
| if (uris.length == 0) { |
| return null; |
| } |
| StringBuilder idSetBuilder = new StringBuilder(); |
| boolean first = true; |
| for (Parcelable p : uris) { |
| Uri uri = (Uri) p; |
| if ("content".equals(uri.getScheme())) { |
| if (first) { |
| first = false; |
| idSetBuilder.append(uri.getLastPathSegment()); |
| } else { |
| idSetBuilder.append(',').append(uri.getLastPathSegment()); |
| } |
| } |
| } |
| // Check whether there is content URI. |
| if (first) return null; |
| Cursor cursor = null; |
| if (idSetBuilder.length() > 0) { |
| final String whereClause = Phone._ID + " IN (" + idSetBuilder.toString() + ")"; |
| cursor = mContext.getContentResolver().query( |
| PHONES_WITH_PRESENCE_URI, CALLER_ID_PROJECTION, whereClause, null, null); |
| } |
| |
| if (cursor == null) { |
| return null; |
| } |
| |
| List<Contact> entries = new ArrayList<Contact>(); |
| |
| try { |
| while (cursor.moveToNext()) { |
| Contact entry = new Contact(cursor.getString(PHONE_NUMBER_COLUMN), |
| cursor.getString(CONTACT_NAME_COLUMN)); |
| fillPhoneTypeContact(entry, cursor); |
| ArrayList<Contact> value = new ArrayList<Contact>(); |
| value.add(entry); |
| // Put the result in the cache. |
| mContactsHash.put(key(entry.mNumber, sStaticKeyBuffer), value); |
| entries.add(entry); |
| } |
| } finally { |
| cursor.close(); |
| } |
| return entries; |
| } |
| |
| private boolean contactChanged(Contact orig, Contact newContactData) { |
| // The phone number should never change, so don't bother checking. |
| // TODO: Maybe update it if it has gotten longer, i.e. 650-234-5678 -> +16502345678? |
| |
| // Do the quick check first. |
| if (orig.mContactMethodType != newContactData.mContactMethodType) { |
| return true; |
| } |
| |
| if (orig.mContactMethodId != newContactData.mContactMethodId) { |
| return true; |
| } |
| |
| if (orig.mPersonId != newContactData.mPersonId) { |
| if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { |
| Log.d(TAG, "person id changed"); |
| } |
| return true; |
| } |
| |
| if (orig.mPresenceResId != newContactData.mPresenceResId) { |
| if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { |
| Log.d(TAG, "presence changed"); |
| } |
| return true; |
| } |
| |
| if (orig.mSendToVoicemail != newContactData.mSendToVoicemail) { |
| return true; |
| } |
| |
| String oldName = emptyIfNull(orig.mName); |
| String newName = emptyIfNull(newContactData.mName); |
| if (!oldName.equals(newName)) { |
| if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { |
| Log.d(TAG, String.format("name changed: %s -> %s", oldName, newName)); |
| } |
| return true; |
| } |
| |
| String oldLabel = emptyIfNull(orig.mLabel); |
| String newLabel = emptyIfNull(newContactData.mLabel); |
| if (!oldLabel.equals(newLabel)) { |
| if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { |
| Log.d(TAG, String.format("label changed: %s -> %s", oldLabel, newLabel)); |
| } |
| return true; |
| } |
| |
| if (!Arrays.equals(orig.mAvatarData, newContactData.mAvatarData)) { |
| if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { |
| Log.d(TAG, "avatar changed"); |
| } |
| return true; |
| } |
| |
| return false; |
| } |
| |
| private void updateContact(final Contact c) { |
| if (c == null) { |
| return; |
| } |
| |
| Contact entry = getContactInfo(c); |
| synchronized (c) { |
| if (contactChanged(c, entry)) { |
| if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { |
| log("updateContact: contact changed for " + entry.mName); |
| } |
| |
| c.mNumber = entry.mNumber; |
| c.mLabel = entry.mLabel; |
| c.mPersonId = entry.mPersonId; |
| c.mPresenceResId = entry.mPresenceResId; |
| c.mPresenceText = entry.mPresenceText; |
| c.mAvatarData = entry.mAvatarData; |
| c.mAvatar = entry.mAvatar; |
| c.mContactMethodId = entry.mContactMethodId; |
| c.mContactMethodType = entry.mContactMethodType; |
| c.mNumberE164 = entry.mNumberE164; |
| c.mName = entry.mName; |
| c.mSendToVoicemail = entry.mSendToVoicemail; |
| |
| c.notSynchronizedUpdateNameAndNumber(); |
| |
| // We saw a bug where we were updating an empty contact. That would trigger |
| // l.onUpdate() below, which would call ComposeMessageActivity.onUpdate, |
| // which would call the adapter's notifyDataSetChanged, which would throw |
| // away the message items and rebuild, eventually calling updateContact() |
| // again -- all in a vicious and unending loop. Break the cycle and don't |
| // notify if the number (the most important piece of information) is empty. |
| if (!TextUtils.isEmpty(c.mNumber)) { |
| // clone the list of listeners in case the onUpdate call turns around and |
| // modifies the list of listeners |
| // access to mListeners is synchronized on ContactsCache |
| HashSet<UpdateListener> iterator; |
| synchronized (mListeners) { |
| iterator = (HashSet<UpdateListener>)Contact.mListeners.clone(); |
| } |
| for (UpdateListener l : iterator) { |
| if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { |
| Log.d(TAG, "updating " + l); |
| } |
| l.onUpdate(c); |
| } |
| } |
| } |
| synchronized (c) { |
| c.mQueryPending = false; |
| c.notifyAll(); |
| } |
| } |
| } |
| |
| /** |
| * Returns the caller info in Contact. |
| */ |
| private Contact getContactInfo(Contact c) { |
| if (c.mIsMe) { |
| return getContactInfoForSelf(); |
| } else if (Mms.isEmailAddress(c.mNumber) || isAlphaNumber(c.mNumber)) { |
| return getContactInfoForEmailAddress(c.mNumber); |
| } else { |
| return getContactInfoForPhoneNumber(c.mNumber); |
| } |
| } |
| |
| // Some received sms's have addresses such as "OakfieldCPS" or "T-Mobile". This |
| // function will attempt to identify these and return true. If the number contains |
| // 3 or more digits, such as "jello123", this function will return false. |
| // Some countries have 3 digits shortcodes and we have to identify them as numbers. |
| // http://en.wikipedia.org/wiki/Short_code |
| // Examples of input/output for this function: |
| // "Jello123" -> false [3 digits, it is considered to be the phone number "123"] |
| // "T-Mobile" -> true [it is considered to be the address "T-Mobile"] |
| // "Mobile1" -> true [1 digit, it is considered to be the address "Mobile1"] |
| // "Dogs77" -> true [2 digits, it is considered to be the address "Dogs77"] |
| // "****1" -> true [1 digits, it is considered to be the address "****1"] |
| // "#4#5#6#" -> true [it is considered to be the address "#4#5#6#"] |
| // "AB12" -> true [2 digits, it is considered to be the address "AB12"] |
| // "12" -> true [2 digits, it is considered to be the address "12"] |
| private boolean isAlphaNumber(String number) { |
| // TODO: PhoneNumberUtils.isWellFormedSmsAddress() only check if the number is a valid |
| // GSM SMS address. If the address contains a dialable char, it considers it a well |
| // formed SMS addr. CDMA doesn't work that way and has a different parser for SMS |
| // address (see CdmaSmsAddress.parse(String address)). We should definitely fix this!!! |
| if (!PhoneNumberUtils.isWellFormedSmsAddress(number)) { |
| // The example "T-Mobile" will exit here because there are no numbers. |
| return true; // we're not an sms address, consider it an alpha number |
| } |
| if (MessageUtils.isAlias(number)) { |
| return true; |
| } |
| number = PhoneNumberUtils.extractNetworkPortion(number); |
| if (TextUtils.isEmpty(number)) { |
| return true; // there are no digits whatsoever in the number |
| } |
| // At this point, anything like "Mobile1" or "Dogs77" will be stripped down to |
| // "1" and "77". "#4#5#6#" remains as "#4#5#6#" at this point. |
| return number.length() < 3; |
| } |
| |
| /** |
| * Queries the caller id info with the phone number. |
| * @return a Contact containing the caller id info corresponding to the number. |
| */ |
| private Contact getContactInfoForPhoneNumber(String number) { |
| number = PhoneNumberUtils.stripSeparators(number); |
| Contact entry = new Contact(number); |
| entry.mContactMethodType = CONTACT_METHOD_TYPE_PHONE; |
| |
| if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { |
| log("queryContactInfoByNumber: number=" + number); |
| } |
| |
| String normalizedNumber = PhoneNumberUtils.normalizeNumber(number); |
| String minMatch = PhoneNumberUtils.toCallerIDMinMatch(normalizedNumber); |
| if (!TextUtils.isEmpty(normalizedNumber) && !TextUtils.isEmpty(minMatch)) { |
| String numberLen = String.valueOf(normalizedNumber.length()); |
| String numberE164 = PhoneNumberUtils.formatNumberToE164( |
| number, MmsApp.getApplication().getCurrentCountryIso()); |
| String selection; |
| String[] args; |
| if (TextUtils.isEmpty(numberE164)) { |
| selection = CALLER_ID_SELECTION_WITHOUT_E164; |
| args = new String[] {minMatch, numberLen, normalizedNumber, numberLen}; |
| } else { |
| selection = CALLER_ID_SELECTION; |
| args = new String[] { |
| minMatch, numberE164, numberLen, normalizedNumber, numberLen}; |
| } |
| |
| Cursor cursor = mContext.getContentResolver().query( |
| PHONES_WITH_PRESENCE_URI, CALLER_ID_PROJECTION, selection, args, null); |
| if (cursor == null) { |
| Log.w(TAG, "queryContactInfoByNumber(" + number + ") returned NULL cursor!" |
| + " contact uri used " + PHONES_WITH_PRESENCE_URI); |
| return entry; |
| } |
| |
| try { |
| if (cursor.moveToFirst()) { |
| fillPhoneTypeContact(entry, cursor); |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| return entry; |
| } |
| |
| /** |
| * @return a Contact containing the info for the profile. |
| */ |
| private Contact getContactInfoForSelf() { |
| Contact entry = new Contact(true); |
| entry.mContactMethodType = CONTACT_METHOD_TYPE_SELF; |
| |
| if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { |
| log("getContactInfoForSelf"); |
| } |
| Cursor cursor = mContext.getContentResolver().query( |
| Profile.CONTENT_URI, SELF_PROJECTION, null, null, null); |
| if (cursor == null) { |
| Log.w(TAG, "getContactInfoForSelf() returned NULL cursor!" |
| + " contact uri used " + Profile.CONTENT_URI); |
| return entry; |
| } |
| |
| try { |
| if (cursor.moveToFirst()) { |
| fillSelfContact(entry, cursor); |
| } |
| } finally { |
| cursor.close(); |
| } |
| return entry; |
| } |
| |
| private void fillPhoneTypeContact(final Contact contact, final Cursor cursor) { |
| synchronized (contact) { |
| contact.mContactMethodType = CONTACT_METHOD_TYPE_PHONE; |
| contact.mContactMethodId = cursor.getLong(PHONE_ID_COLUMN); |
| contact.mLabel = cursor.getString(PHONE_LABEL_COLUMN); |
| contact.mName = cursor.getString(CONTACT_NAME_COLUMN); |
| contact.mPersonId = cursor.getLong(CONTACT_ID_COLUMN); |
| contact.mPresenceResId = getPresenceIconResourceId( |
| cursor.getInt(CONTACT_PRESENCE_COLUMN)); |
| contact.mPresenceText = cursor.getString(CONTACT_STATUS_COLUMN); |
| contact.mNumberE164 = cursor.getString(PHONE_NORMALIZED_NUMBER); |
| contact.mSendToVoicemail = cursor.getInt(SEND_TO_VOICEMAIL) == 1; |
| if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { |
| log("fillPhoneTypeContact: name=" + contact.mName + ", number=" |
| + contact.mNumber + ", presence=" + contact.mPresenceResId |
| + " SendToVoicemail: " + contact.mSendToVoicemail); |
| } |
| } |
| byte[] data = loadAvatarData(contact); |
| |
| synchronized (contact) { |
| contact.mAvatarData = data; |
| } |
| } |
| |
| private void fillSelfContact(final Contact contact, final Cursor cursor) { |
| synchronized (contact) { |
| contact.mName = cursor.getString(SELF_NAME_COLUMN); |
| if (TextUtils.isEmpty(contact.mName)) { |
| contact.mName = mContext.getString(R.string.messagelist_sender_self); |
| } |
| if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { |
| log("fillSelfContact: name=" + contact.mName + ", number=" |
| + contact.mNumber); |
| } |
| } |
| byte[] data = loadAvatarData(contact); |
| |
| synchronized (contact) { |
| contact.mAvatarData = data; |
| } |
| } |
| /* |
| * Load the avatar data from the cursor into memory. Don't decode the data |
| * until someone calls for it (see getAvatar). Hang onto the raw data so that |
| * we can compare it when the data is reloaded. |
| * TODO: consider comparing a checksum so that we don't have to hang onto |
| * the raw bytes after the image is decoded. |
| */ |
| private byte[] loadAvatarData(Contact entry) { |
| byte [] data = null; |
| |
| if ((!entry.mIsMe && entry.mPersonId == 0) || entry.mAvatar != null) { |
| return null; |
| } |
| |
| if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { |
| log("loadAvatarData: name=" + entry.mName + ", number=" + entry.mNumber); |
| } |
| |
| // If the contact is "me", then use my local profile photo. Otherwise, build a |
| // uri to get the avatar of the contact. |
| Uri contactUri = entry.mIsMe ? |
| Profile.CONTENT_URI : |
| ContentUris.withAppendedId(Contacts.CONTENT_URI, entry.mPersonId); |
| |
| InputStream avatarDataStream = Contacts.openContactPhotoInputStream( |
| mContext.getContentResolver(), |
| contactUri); |
| try { |
| if (avatarDataStream != null) { |
| data = new byte[avatarDataStream.available()]; |
| avatarDataStream.read(data, 0, data.length); |
| } |
| } catch (IOException ex) { |
| // |
| } finally { |
| try { |
| if (avatarDataStream != null) { |
| avatarDataStream.close(); |
| } |
| } catch (IOException e) { |
| } |
| } |
| |
| return data; |
| } |
| |
| private int getPresenceIconResourceId(int presence) { |
| // TODO: must fix for SDK |
| if (presence != Presence.OFFLINE) { |
| return Presence.getPresenceIconResourceId(presence); |
| } |
| |
| return 0; |
| } |
| |
| /** |
| * Query the contact email table to get the name of an email address. |
| */ |
| private Contact getContactInfoForEmailAddress(String email) { |
| Contact entry = new Contact(email); |
| entry.mContactMethodType = CONTACT_METHOD_TYPE_EMAIL; |
| |
| Cursor cursor = SqliteWrapper.query(mContext, mContext.getContentResolver(), |
| EMAIL_WITH_PRESENCE_URI, |
| EMAIL_PROJECTION, |
| EMAIL_SELECTION, |
| new String[] { email }, |
| null); |
| |
| if (cursor != null) { |
| try { |
| while (cursor.moveToNext()) { |
| boolean found = false; |
| synchronized (entry) { |
| entry.mContactMethodId = cursor.getLong(EMAIL_ID_COLUMN); |
| entry.mPresenceResId = getPresenceIconResourceId( |
| cursor.getInt(EMAIL_STATUS_COLUMN)); |
| entry.mPersonId = cursor.getLong(EMAIL_CONTACT_ID_COLUMN); |
| entry.mSendToVoicemail = |
| cursor.getInt(EMAIL_SEND_TO_VOICEMAIL_COLUMN) == 1; |
| |
| String name = cursor.getString(EMAIL_NAME_COLUMN); |
| if (TextUtils.isEmpty(name)) { |
| name = cursor.getString(EMAIL_CONTACT_NAME_COLUMN); |
| } |
| if (!TextUtils.isEmpty(name)) { |
| entry.mName = name; |
| if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) { |
| log("getContactInfoForEmailAddress: name=" + entry.mName + |
| ", email=" + email + ", presence=" + |
| entry.mPresenceResId); |
| } |
| found = true; |
| } |
| } |
| |
| if (found) { |
| byte[] data = loadAvatarData(entry); |
| synchronized (entry) { |
| entry.mAvatarData = data; |
| } |
| |
| break; |
| } |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| return entry; |
| } |
| |
| // Invert and truncate to five characters the phoneNumber so that we |
| // can use it as the key in a hashtable. We keep a mapping of this |
| // key to a list of all contacts which have the same key. |
| private String key(String phoneNumber, CharBuffer keyBuffer) { |
| keyBuffer.clear(); |
| keyBuffer.mark(); |
| |
| int position = phoneNumber.length(); |
| int resultCount = 0; |
| while (--position >= 0) { |
| char c = phoneNumber.charAt(position); |
| if (Character.isDigit(c)) { |
| keyBuffer.put(c); |
| if (++resultCount == STATIC_KEY_BUFFER_MAXIMUM_LENGTH) { |
| break; |
| } |
| } |
| } |
| keyBuffer.reset(); |
| if (resultCount > 0) { |
| return keyBuffer.toString(); |
| } else { |
| // there were no usable digits in the input phoneNumber |
| return phoneNumber; |
| } |
| } |
| |
| // Reuse this so we don't have to allocate each time we go through this |
| // "get" function. |
| static final int STATIC_KEY_BUFFER_MAXIMUM_LENGTH = 5; |
| static CharBuffer sStaticKeyBuffer = CharBuffer.allocate(STATIC_KEY_BUFFER_MAXIMUM_LENGTH); |
| |
| private Contact internalGet(String numberOrEmail, boolean isMe) { |
| synchronized (ContactsCache.this) { |
| // See if we can find "number" in the hashtable. |
| // If so, just return the result. |
| final boolean isNotRegularPhoneNumber = isMe || Mms.isEmailAddress(numberOrEmail) || |
| MessageUtils.isAlias(numberOrEmail); |
| final String key = isNotRegularPhoneNumber ? |
| numberOrEmail : key(numberOrEmail, sStaticKeyBuffer); |
| |
| ArrayList<Contact> candidates = mContactsHash.get(key); |
| if (candidates != null) { |
| int length = candidates.size(); |
| for (int i = 0; i < length; i++) { |
| Contact c= candidates.get(i); |
| if (isNotRegularPhoneNumber) { |
| if (numberOrEmail.equals(c.mNumber)) { |
| return c; |
| } |
| } else { |
| if (PhoneNumberUtils.compare(numberOrEmail, c.mNumber)) { |
| return c; |
| } |
| } |
| } |
| } else { |
| candidates = new ArrayList<Contact>(); |
| // call toString() since it may be the static CharBuffer |
| mContactsHash.put(key, candidates); |
| } |
| Contact c = isMe ? |
| new Contact(true) : |
| new Contact(numberOrEmail); |
| candidates.add(c); |
| return c; |
| } |
| } |
| |
| void invalidate() { |
| // Don't remove the contacts. Just mark them stale so we'll update their |
| // info, particularly their presence. |
| synchronized (ContactsCache.this) { |
| for (ArrayList<Contact> alc : mContactsHash.values()) { |
| for (Contact c : alc) { |
| synchronized (c) { |
| c.mIsStale = true; |
| } |
| } |
| } |
| } |
| } |
| |
| // Remove a contact from the ContactsCache based on the number or email address |
| private void remove(Contact contact) { |
| synchronized (ContactsCache.this) { |
| String number = contact.getNumber(); |
| final boolean isNotRegularPhoneNumber = contact.isMe() || |
| Mms.isEmailAddress(number) || |
| MessageUtils.isAlias(number); |
| final String key = isNotRegularPhoneNumber ? |
| number : key(number, sStaticKeyBuffer); |
| ArrayList<Contact> candidates = mContactsHash.get(key); |
| if (candidates != null) { |
| int length = candidates.size(); |
| for (int i = 0; i < length; i++) { |
| Contact c = candidates.get(i); |
| if (isNotRegularPhoneNumber) { |
| if (number.equals(c.mNumber)) { |
| candidates.remove(i); |
| break; |
| } |
| } else { |
| if (PhoneNumberUtils.compare(number, c.mNumber)) { |
| candidates.remove(i); |
| break; |
| } |
| } |
| } |
| if (candidates.size() == 0) { |
| mContactsHash.remove(key); |
| } |
| } |
| } |
| } |
| } |
| |
| private static void log(String msg) { |
| Log.d(TAG, msg); |
| } |
| } |