• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.mms.data;
2 
3 import java.util.ArrayList;
4 import java.util.HashSet;
5 import java.util.List;
6 
7 import android.content.ContentUris;
8 import android.content.Context;
9 import android.database.ContentObserver;
10 import android.graphics.drawable.BitmapDrawable;
11 import android.graphics.drawable.Drawable;
12 import android.net.Uri;
13 import android.os.Handler;
14 import android.provider.ContactsContract.Contacts;
15 import android.provider.ContactsContract.Presence;
16 import android.provider.Telephony.Mms;
17 import android.telephony.PhoneNumberUtils;
18 import android.text.TextUtils;
19 import android.util.Log;
20 
21 import com.android.mms.ui.MessageUtils;
22 import com.android.mms.util.ContactInfoCache;
23 import com.android.mms.util.TaskStack;
24 import com.android.mms.LogTag;
25 
26 public 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
toString()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 {
onUpdate(Contact updated)74         public void onUpdate(Contact updated);
75     }
76 
Contact(String number)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 
logWithTrace(String msg, Object... format)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 
get(String number, boolean canBlock)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 
invalidateCache()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 
emptyIfNull(String s)149     private static String emptyIfNull(String s) {
150         return (s != null ? s : "");
151     }
152 
contactChanged(Contact orig, ContactInfoCache.CacheEntry newEntry)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      */
handleLocalNumber(Contact c)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 
asyncUpdateContact(final Contact c, boolean canBlock)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 
updateContact(final Contact c)220     private static void updateContact(final Contact c) {
221         if (c == null) {
222             return;
223         }
224         c.mIsStale = false;
225 
226         // Check to see if this is the local ("me") number.
227         if (handleLocalNumber(c)) {
228             return;
229         }
230 
231         ContactInfoCache cache = ContactInfoCache.getInstance();
232         ContactInfoCache.CacheEntry entry = cache.getContactInfo(c.mNumber);
233         synchronized (Cache.getInstance()) {
234             if (contactChanged(c, entry)) {
235                 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
236                     log("updateContact: contact changed for " + entry.name);
237                 }
238 
239                 //c.mNumber = entry.phoneNumber;
240                 c.mName = entry.name;
241                 c.updateNameAndNumber();
242                 c.mLabel = entry.phoneLabel;
243                 c.mPersonId = entry.person_id;
244                 c.mPresenceResId = entry.presenceResId;
245                 c.mPresenceText = entry.presenceText;
246                 c.mAvatar = entry.mAvatar;
247                 for (UpdateListener l : c.mListeners) {
248                     if (V) Log.d(TAG, "updating " + l);
249                     l.onUpdate(c);
250                 }
251             }
252         }
253     }
254 
formatNameAndNumber(String name, String number)255     public static String formatNameAndNumber(String name, String number) {
256         // Format like this: Mike Cleron <(650) 555-1234>
257         //                   Erick Tseng <(650) 555-1212>
258         //                   Tutankhamun <tutank1341@gmail.com>
259         //                   (408) 555-1289
260         String formattedNumber = number;
261         if (!Mms.isEmailAddress(number)) {
262             formattedNumber = PhoneNumberUtils.formatNumber(number);
263         }
264 
265         if (!TextUtils.isEmpty(name) && !name.equals(number)) {
266             return name + " <" + formattedNumber + ">";
267         } else {
268             return formattedNumber;
269         }
270     }
271 
getNumber()272     public synchronized String getNumber() {
273         return mNumber;
274     }
275 
setNumber(String number)276     public synchronized void setNumber(String number) {
277         mNumber = number;
278         updateNameAndNumber();
279         mNumberIsModified = true;
280     }
281 
isNumberModified()282     public boolean isNumberModified() {
283         return mNumberIsModified;
284     }
285 
setIsNumberModified(boolean flag)286     public void setIsNumberModified(boolean flag) {
287         mNumberIsModified = flag;
288     }
289 
getName()290     public synchronized String getName() {
291         if (TextUtils.isEmpty(mName)) {
292             return mNumber;
293         } else {
294             return mName;
295         }
296     }
297 
getNameAndNumber()298     public synchronized String getNameAndNumber() {
299         return mNameAndNumber;
300     }
301 
updateNameAndNumber()302     private void updateNameAndNumber() {
303         mNameAndNumber = formatNameAndNumber(mName, mNumber);
304     }
305 
getRecipientId()306     public synchronized long getRecipientId() {
307         return mRecipientId;
308     }
309 
setRecipientId(long id)310     public synchronized void setRecipientId(long id) {
311         mRecipientId = id;
312     }
313 
getLabel()314     public synchronized String getLabel() {
315         return mLabel;
316     }
317 
getUri()318     public synchronized Uri getUri() {
319         return ContentUris.withAppendedId(Contacts.CONTENT_URI, mPersonId);
320     }
321 
getPersonId()322     public long getPersonId() {
323         return mPersonId;
324     }
325 
getPresenceResId()326     public synchronized int getPresenceResId() {
327         return mPresenceResId;
328     }
329 
existsInDatabase()330     public synchronized boolean existsInDatabase() {
331         return (mPersonId > 0);
332     }
333 
addListener(UpdateListener l)334     public synchronized void addListener(UpdateListener l) {
335         boolean added = mListeners.add(l);
336         if (V && added) dumpListeners();
337     }
338 
removeListener(UpdateListener l)339     public synchronized void removeListener(UpdateListener l) {
340         boolean removed = mListeners.remove(l);
341         if (V && removed) dumpListeners();
342     }
343 
dumpListeners()344     public synchronized void dumpListeners() {
345         int i=0;
346         Log.i(TAG, "[Contact] dumpListeners(" + mNumber + ") size=" + mListeners.size());
347         for (UpdateListener listener : mListeners) {
348             Log.i(TAG, "["+ (i++) + "]" + listener);
349         }
350     }
351 
isEmail()352     public synchronized boolean isEmail() {
353         return Mms.isEmailAddress(mNumber);
354     }
355 
getPresenceText()356     public String getPresenceText() {
357         return mPresenceText;
358     }
359 
getAvatar(Drawable defaultValue)360     public Drawable getAvatar(Drawable defaultValue) {
361         return mAvatar != null ? mAvatar : defaultValue;
362     }
363 
init(final Context context)364     public static void init(final Context context) {
365         Cache.init(context);
366         RecipientIdCache.init(context);
367 
368         // it maybe too aggressive to listen for *any* contact changes, and rebuild MMS contact
369         // cache each time that occurs. Unless we can get targeted updates for the contacts we
370         // care about(which probably won't happen for a long time), we probably should just
371         // invalidate cache peoridically, or surgically.
372         /*
373         context.getContentResolver().registerContentObserver(
374                 Contacts.CONTENT_URI, true, sContactsObserver);
375         */
376     }
377 
dump()378     public static void dump() {
379         Cache.dump();
380     }
381 
startPresenceObserver()382     public static void startPresenceObserver() {
383         Cache.getContext().getContentResolver().registerContentObserver(
384                 Presence.CONTENT_URI, true, sPresenceObserver);
385     }
386 
stopPresenceObserver()387     public static void stopPresenceObserver() {
388         Cache.getContext().getContentResolver().unregisterContentObserver(sPresenceObserver);
389     }
390 
391     private static class Cache {
392         private static Cache sInstance;
getInstance()393         static Cache getInstance() { return sInstance; }
394         private final List<Contact> mCache;
395         private final Context mContext;
Cache(Context context)396         private Cache(Context context) {
397             mCache = new ArrayList<Contact>();
398             mContext = context;
399         }
400 
init(Context context)401         static void init(Context context) {
402             sInstance = new Cache(context);
403         }
404 
getContext()405         static Context getContext() {
406             return sInstance.mContext;
407         }
408 
dump()409         static void dump() {
410             synchronized (sInstance) {
411                 Log.d(TAG, "**** Contact cache dump ****");
412                 for (Contact c : sInstance.mCache) {
413                     Log.d(TAG, c.toString());
414                 }
415             }
416         }
417 
getEmail(String number)418         private static Contact getEmail(String number) {
419             synchronized (sInstance) {
420                 for (Contact c : sInstance.mCache) {
421                     if (number.equalsIgnoreCase(c.mNumber)) {
422                         return c;
423                     }
424                 }
425                 return null;
426             }
427         }
428 
get(String number)429         static Contact get(String number) {
430             if (Mms.isEmailAddress(number))
431                 return getEmail(number);
432 
433             synchronized (sInstance) {
434                 for (Contact c : sInstance.mCache) {
435 
436                     // if the numbers are an exact match (i.e. Google SMS), or if the phone
437                     // number comparison returns a match, return the contact.
438                     if (number.equals(c.mNumber) || PhoneNumberUtils.compare(number, c.mNumber)) {
439                         return c;
440                     }
441                 }
442                 return null;
443             }
444         }
445 
put(Contact c)446         static void put(Contact c) {
447             synchronized (sInstance) {
448                 // We update cache entries in place so people with long-
449                 // held references get updated.
450                 if (get(c.mNumber) != null) {
451                     throw new IllegalStateException("cache already contains " + c);
452                 }
453                 sInstance.mCache.add(c);
454             }
455         }
456 
getNumbers()457         static String[] getNumbers() {
458             synchronized (sInstance) {
459                 String[] numbers = new String[sInstance.mCache.size()];
460                 int i = 0;
461                 for (Contact c : sInstance.mCache) {
462                     numbers[i++] = c.getNumber();
463                 }
464                 return numbers;
465             }
466         }
467 
getContacts()468         static List<Contact> getContacts() {
469             synchronized (sInstance) {
470                 return new ArrayList<Contact>(sInstance.mCache);
471             }
472         }
473 
invalidate()474         static void invalidate() {
475             // Don't remove the contacts. Just mark them stale so we'll update their
476             // info, particularly their presence.
477             synchronized (sInstance) {
478                 for (Contact c : sInstance.mCache) {
479                     c.mIsStale = true;
480                 }
481             }
482         }
483     }
484 
log(String msg)485     private static void log(String msg) {
486         Log.d(TAG, msg);
487     }
488 }
489