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