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