Contact.java revision 6646f0f7e8fa5797926f93938a3b4ab1018ea4d1
1package com.android.mms.data;
2
3import java.util.ArrayList;
4import java.util.HashSet;
5import java.util.List;
6
7import android.content.ContentUris;
8import android.content.Context;
9import android.database.ContentObserver;
10import android.graphics.drawable.BitmapDrawable;
11import android.graphics.drawable.Drawable;
12import android.net.Uri;
13import android.os.Handler;
14import android.provider.ContactsContract.Contacts;
15import android.provider.ContactsContract.Presence;
16import android.provider.Telephony.Mms;
17import android.telephony.PhoneNumberUtils;
18import android.text.TextUtils;
19import android.util.Log;
20
21import com.android.mms.ui.MessageUtils;
22import com.android.mms.util.ContactInfoCache;
23import com.android.mms.util.TaskStack;
24import com.android.mms.LogTag;
25
26public class Contact {
27    private static final String TAG = "Contact";
28    private static final boolean V = false;
29
30    private static final TaskStack sTaskStack = new TaskStack();
31
32//    private static final ContentObserver sContactsObserver = new ContentObserver(new Handler()) {
33//        @Override
34//        public void onChange(boolean selfUpdate) {
35//            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
36//                log("contact changed, invalidate cache");
37//            }
38//            invalidateCache();
39//        }
40//    };
41
42    private static final ContentObserver sPresenceObserver = new ContentObserver(new Handler()) {
43        @Override
44        public void onChange(boolean selfUpdate) {
45            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
46                log("presence changed, invalidate cache");
47            }
48            invalidateCache();
49        }
50    };
51
52    private final HashSet<UpdateListener> mListeners = new HashSet<UpdateListener>();
53
54    private String mNumber;
55    private String mName;
56    private String mNameAndNumber;   // for display, e.g. Fred Flintstone <670-782-1123>
57    private boolean mNumberIsModified; // true if the number is modified
58
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 boolean mIsStale;
66
67    @Override
68    public synchronized String toString() {
69        return String.format("{ number=%s, name=%s, nameAndNumber=%s, label=%s, person_id=%d }",
70                mNumber, mName, mNameAndNumber, mLabel, mPersonId);
71    }
72
73    public interface UpdateListener {
74        public void onUpdate(Contact updated);
75    }
76
77    private Contact(String number) {
78        mName = "";
79        setNumber(number);
80        mNumberIsModified = false;
81        mLabel = "";
82        mPersonId = 0;
83        mPresenceResId = 0;
84        mIsStale = true;
85    }
86
87    private static void logWithTrace(String msg, Object... format) {
88        Thread current = Thread.currentThread();
89        StackTraceElement[] stack = current.getStackTrace();
90
91        StringBuilder sb = new StringBuilder();
92        sb.append("[");
93        sb.append(current.getId());
94        sb.append("] ");
95        sb.append(String.format(msg, format));
96
97        sb.append(" <- ");
98        int stop = stack.length > 7 ? 7 : stack.length;
99        for (int i = 3; i < stop; i++) {
100            String methodName = stack[i].getMethodName();
101            sb.append(methodName);
102            if ((i+1) != stop) {
103                sb.append(" <- ");
104            }
105        }
106
107        Log.d(TAG, sb.toString());
108    }
109
110    public static Contact get(String number, boolean canBlock) {
111        if (V) logWithTrace("get(%s, %s)", number, canBlock);
112
113        if (TextUtils.isEmpty(number)) {
114            throw new IllegalArgumentException("Contact.get called with null or empty number");
115        }
116
117        Contact contact = Cache.get(number);
118        if (contact == null) {
119            contact = new Contact(number);
120            Cache.put(contact);
121        }
122        if (contact.mIsStale) {
123            asyncUpdateContact(contact, canBlock);
124        }
125        return contact;
126    }
127
128    public static void invalidateCache() {
129        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
130            log("invalidateCache");
131        }
132
133        // force invalidate the contact info cache, so we will query for fresh info again.
134        // This is so we can get fresh presence info again on the screen, since the presence
135        // info changes pretty quickly, and we can't get change notifications when presence is
136        // updated in the ContactsProvider.
137        ContactInfoCache.getInstance().invalidateCache();
138
139        // While invalidating our local Cache doesn't remove the contacts, it will mark them
140        // stale so the next time we're asked for a particular contact, we'll return that
141        // stale contact and at the same time, fire off an asyncUpdateContact to update
142        // that contact's info in the background. UI elements using the contact typically
143        // call addListener() so they immediately get notified when the contact has been
144        // updated with the latest info. They redraw themselves when we call the
145        // listener's onUpdate().
146        Cache.invalidate();
147    }
148
149    private static String emptyIfNull(String s) {
150        return (s != null ? s : "");
151    }
152
153    private static boolean contactChanged(Contact orig, ContactInfoCache.CacheEntry newEntry) {
154        // The phone number should never change, so don't bother checking.
155        // TODO: Maybe update it if it has gotten longer, i.e. 650-234-5678 -> +16502345678?
156
157        String oldName = emptyIfNull(orig.mName);
158        String newName = emptyIfNull(newEntry.name);
159        if (!oldName.equals(newName)) {
160            if (V) Log.d(TAG, String.format("name changed: %s -> %s", oldName, newName));
161            return true;
162        }
163
164        String oldLabel = emptyIfNull(orig.mLabel);
165        String newLabel = emptyIfNull(newEntry.phoneLabel);
166        if (!oldLabel.equals(newLabel)) {
167            if (V) Log.d(TAG, String.format("label changed: %s -> %s", oldLabel, newLabel));
168            return true;
169        }
170
171        if (orig.mPersonId != newEntry.person_id) {
172            if (V) Log.d(TAG, "person id changed");
173            return true;
174        }
175
176        if (orig.mPresenceResId != newEntry.presenceResId) {
177            if (V) Log.d(TAG, "presence changed");
178            return true;
179        }
180
181        return false;
182    }
183
184    /**
185     * Handles the special case where the local ("Me") number is being looked up.
186     * Updates the contact with the "me" name and returns true if it is the
187     * local number, no-ops and returns false if it is not.
188     */
189    private static boolean handleLocalNumber(Contact c) {
190        if (MessageUtils.isLocalNumber(c.mNumber)) {
191            c.mName = Cache.getContext().getString(com.android.internal.R.string.me);
192            c.updateNameAndNumber();
193            return true;
194        }
195        return false;
196    }
197
198    private static void asyncUpdateContact(final Contact c, boolean canBlock) {
199        if (c == null) {
200            return;
201        }
202
203        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
204            log("asyncUpdateContact for " + c.toString());
205        }
206
207        Runnable r = new Runnable() {
208            public void run() {
209                updateContact(c);
210            }
211        };
212
213        if (canBlock) {
214            r.run();
215        } else {
216            sTaskStack.push(r);
217        }
218    }
219
220    private static void updateContact(final Contact c) {
221        if (c == null) {
222            return;
223        }
224        c.mIsStale = false;
225
226        ContactInfoCache cache = ContactInfoCache.getInstance();
227        ContactInfoCache.CacheEntry entry = cache.getContactInfo(c.mNumber);
228        synchronized (Cache.getInstance()) {
229            if (contactChanged(c, entry)) {
230                if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
231                    log("updateContact: contact changed for " + entry.name);
232                }
233
234                //c.mNumber = entry.phoneNumber;
235                c.mName = entry.name;
236                c.updateNameAndNumber();
237                c.mLabel = entry.phoneLabel;
238                c.mPersonId = entry.person_id;
239                c.mPresenceResId = entry.presenceResId;
240                c.mPresenceText = entry.presenceText;
241                c.mAvatar = entry.mAvatar;
242
243                // Check to see if this is the local ("me") number and update the name.
244                handleLocalNumber(c);
245
246                for (UpdateListener l : c.mListeners) {
247                    if (V) Log.d(TAG, "updating " + l);
248                    l.onUpdate(c);
249                }
250            }
251        }
252    }
253
254    public static String formatNameAndNumber(String name, String number) {
255        // Format like this: Mike Cleron <(650) 555-1234>
256        //                   Erick Tseng <(650) 555-1212>
257        //                   Tutankhamun <tutank1341@gmail.com>
258        //                   (408) 555-1289
259        String formattedNumber = number;
260        if (!Mms.isEmailAddress(number)) {
261            formattedNumber = PhoneNumberUtils.formatNumber(number);
262        }
263
264        if (!TextUtils.isEmpty(name) && !name.equals(number)) {
265            return name + " <" + formattedNumber + ">";
266        } else {
267            return formattedNumber;
268        }
269    }
270
271    public synchronized String getNumber() {
272        return mNumber;
273    }
274
275    public synchronized void setNumber(String number) {
276        mNumber = number;
277        updateNameAndNumber();
278        mNumberIsModified = true;
279    }
280
281    public boolean isNumberModified() {
282        return mNumberIsModified;
283    }
284
285    public void setIsNumberModified(boolean flag) {
286        mNumberIsModified = flag;
287    }
288
289    public synchronized String getName() {
290        if (TextUtils.isEmpty(mName)) {
291            return mNumber;
292        } else {
293            return mName;
294        }
295    }
296
297    public synchronized String getNameAndNumber() {
298        return mNameAndNumber;
299    }
300
301    private void updateNameAndNumber() {
302        mNameAndNumber = formatNameAndNumber(mName, mNumber);
303    }
304
305    public synchronized long getRecipientId() {
306        return mRecipientId;
307    }
308
309    public synchronized void setRecipientId(long id) {
310        mRecipientId = id;
311    }
312
313    public synchronized String getLabel() {
314        return mLabel;
315    }
316
317    public synchronized Uri getUri() {
318        return ContentUris.withAppendedId(Contacts.CONTENT_URI, mPersonId);
319    }
320
321    public long getPersonId() {
322        return mPersonId;
323    }
324
325    public synchronized int getPresenceResId() {
326        return mPresenceResId;
327    }
328
329    public synchronized boolean existsInDatabase() {
330        return (mPersonId > 0);
331    }
332
333    public synchronized void addListener(UpdateListener l) {
334        boolean added = mListeners.add(l);
335        if (V && added) dumpListeners();
336    }
337
338    public synchronized void removeListener(UpdateListener l) {
339        boolean removed = mListeners.remove(l);
340        if (V && removed) dumpListeners();
341    }
342
343    public synchronized void dumpListeners() {
344        int i=0;
345        Log.i(TAG, "[Contact] dumpListeners(" + mNumber + ") size=" + mListeners.size());
346        for (UpdateListener listener : mListeners) {
347            Log.i(TAG, "["+ (i++) + "]" + listener);
348        }
349    }
350
351    public synchronized boolean isEmail() {
352        return Mms.isEmailAddress(mNumber);
353    }
354
355    public String getPresenceText() {
356        return mPresenceText;
357    }
358
359    public Drawable getAvatar(Drawable defaultValue) {
360        return mAvatar != null ? mAvatar : defaultValue;
361    }
362
363    public static void init(final Context context) {
364        Cache.init(context);
365        RecipientIdCache.init(context);
366
367        // it maybe too aggressive to listen for *any* contact changes, and rebuild MMS contact
368        // cache each time that occurs. Unless we can get targeted updates for the contacts we
369        // care about(which probably won't happen for a long time), we probably should just
370        // invalidate cache peoridically, or surgically.
371        /*
372        context.getContentResolver().registerContentObserver(
373                Contacts.CONTENT_URI, true, sContactsObserver);
374        */
375    }
376
377    public static void dump() {
378        Cache.dump();
379    }
380
381    public static void startPresenceObserver() {
382        Cache.getContext().getContentResolver().registerContentObserver(
383                Presence.CONTENT_URI, true, sPresenceObserver);
384    }
385
386    public static void stopPresenceObserver() {
387        Cache.getContext().getContentResolver().unregisterContentObserver(sPresenceObserver);
388    }
389
390    private static class Cache {
391        private static Cache sInstance;
392        static Cache getInstance() { return sInstance; }
393        private final List<Contact> mCache;
394        private final Context mContext;
395        private Cache(Context context) {
396            mCache = new ArrayList<Contact>();
397            mContext = context;
398        }
399
400        static void init(Context context) {
401            sInstance = new Cache(context);
402        }
403
404        static Context getContext() {
405            return sInstance.mContext;
406        }
407
408        static void dump() {
409            synchronized (sInstance) {
410                Log.d(TAG, "**** Contact cache dump ****");
411                for (Contact c : sInstance.mCache) {
412                    Log.d(TAG, c.toString());
413                }
414            }
415        }
416
417        private static Contact getEmail(String number) {
418            synchronized (sInstance) {
419                for (Contact c : sInstance.mCache) {
420                    if (number.equalsIgnoreCase(c.mNumber)) {
421                        return c;
422                    }
423                }
424                return null;
425            }
426        }
427
428        static Contact get(String number) {
429            if (Mms.isEmailAddress(number))
430                return getEmail(number);
431
432            synchronized (sInstance) {
433                for (Contact c : sInstance.mCache) {
434
435                    // if the numbers are an exact match (i.e. Google SMS), or if the phone
436                    // number comparison returns a match, return the contact.
437                    if (number.equals(c.mNumber) || PhoneNumberUtils.compare(number, c.mNumber)) {
438                        return c;
439                    }
440                }
441                return null;
442            }
443        }
444
445        static void put(Contact c) {
446            synchronized (sInstance) {
447                // We update cache entries in place so people with long-
448                // held references get updated.
449                if (get(c.mNumber) != null) {
450                    throw new IllegalStateException("cache already contains " + c);
451                }
452                sInstance.mCache.add(c);
453            }
454        }
455
456        static String[] getNumbers() {
457            synchronized (sInstance) {
458                String[] numbers = new String[sInstance.mCache.size()];
459                int i = 0;
460                for (Contact c : sInstance.mCache) {
461                    numbers[i++] = c.getNumber();
462                }
463                return numbers;
464            }
465        }
466
467        static List<Contact> getContacts() {
468            synchronized (sInstance) {
469                return new ArrayList<Contact>(sInstance.mCache);
470            }
471        }
472
473        static void invalidate() {
474            // Don't remove the contacts. Just mark them stale so we'll update their
475            // info, particularly their presence.
476            synchronized (sInstance) {
477                for (Contact c : sInstance.mCache) {
478                    c.mIsStale = true;
479                }
480            }
481        }
482    }
483
484    private static void log(String msg) {
485        Log.d(TAG, msg);
486    }
487}
488