1 /* 2 * Copyright (C) 2008 Esmertec AG. 3 * Copyright (C) 2008 The Android Open Source Project 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package com.android.mms.util; 19 20 import com.android.mms.ui.MessageUtils; 21 import com.google.android.mms.util.SqliteWrapper; 22 23 import android.content.ContentUris; 24 import android.content.Context; 25 import android.database.Cursor; 26 import android.graphics.Bitmap; 27 import android.graphics.BitmapFactory; 28 import android.graphics.drawable.BitmapDrawable; 29 import android.net.Uri; 30 import android.provider.ContactsContract.Contacts; 31 import android.provider.ContactsContract.Data; 32 import android.provider.ContactsContract.Presence; 33 import android.provider.ContactsContract.CommonDataKinds.Email; 34 import android.provider.ContactsContract.CommonDataKinds.Phone; 35 import android.provider.Telephony.Mms; 36 import android.telephony.PhoneNumberUtils; 37 import android.text.TextUtils; 38 import android.util.Log; 39 40 import java.io.IOException; 41 import java.io.InputStream; 42 import java.util.HashMap; 43 import java.util.Map; 44 import java.util.regex.Matcher; 45 46 /** 47 * This class caches query results of contact database and provides convenient 48 * methods to return contact display name, etc. 49 * 50 * TODO: To improve performance, we should make contacts query by ourselves instead of 51 * doing it one by one calling the CallerInfo API. In the long term, the contacts 52 * database could have a caching layer to ease the work for all apps. 53 */ 54 public class ContactInfoCache { 55 private static final String TAG = "Mms/cache"; 56 57 private static final boolean LOCAL_DEBUG = false; 58 59 private static final String SEPARATOR = ";"; 60 61 // query params for caller id lookup 62 private static final String CALLER_ID_SELECTION = "PHONE_NUMBERS_EQUAL(" + Phone.NUMBER 63 + ",?) AND " + Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'"; 64 65 // Utilizing private API 66 private static final Uri PHONES_WITH_PRESENCE_URI = Data.CONTENT_URI; 67 68 private static final String[] CALLER_ID_PROJECTION = new String[] { 69 Phone.NUMBER, // 0 70 Phone.LABEL, // 1 71 Phone.DISPLAY_NAME, // 2 72 Phone.CONTACT_ID, // 3 73 Phone.CONTACT_PRESENCE, // 4 74 Phone.CONTACT_STATUS, // 5 75 }; 76 private static final int PHONE_NUMBER_COLUMN = 0; 77 private static final int PHONE_LABEL_COLUMN = 1; 78 private static final int CONTACT_NAME_COLUMN = 2; 79 private static final int CONTACT_ID_COLUMN = 3; 80 private static final int CONTACT_PRESENCE_COLUMN = 4; 81 private static final int CONTACT_STATUS_COLUMN = 5; 82 83 // query params for contact lookup by email 84 private static final Uri EMAIL_WITH_PRESENCE_URI = Data.CONTENT_URI; 85 86 private static final String EMAIL_SELECTION = Email.DATA + "=? AND " + Data.MIMETYPE + "='" 87 + Email.CONTENT_ITEM_TYPE + "'"; 88 89 private static final String[] EMAIL_PROJECTION = new String[] { 90 Email.DISPLAY_NAME, // 0 91 Email.CONTACT_PRESENCE, // 1 92 Email.CONTACT_ID, // 2 93 Phone.DISPLAY_NAME, // 94 }; 95 private static final int EMAIL_NAME_COLUMN = 0; 96 private static final int EMAIL_STATUS_COLUMN = 1; 97 private static final int EMAIL_ID_COLUMN = 2; 98 private static final int EMAIL_CONTACT_NAME_COLUMN = 3; 99 100 private static ContactInfoCache sInstance; 101 102 private final Context mContext; 103 104 private String[] mContactInfoSelectionArgs = new String[1]; 105 106 // cached contact info 107 private final HashMap<String, CacheEntry> mCache = new HashMap<String, CacheEntry>(); 108 109 // for background cache rebuilding 110 private Thread mCacheRebuilder = null; 111 private Object mCacheRebuildLock = new Object(); 112 private boolean mPhoneCacheInvalidated = false; 113 private boolean mEmailCacheInvalidated = false; 114 115 /** 116 * CacheEntry stores the caller id or email lookup info. 117 */ 118 public class CacheEntry { 119 /** 120 * phone number 121 */ 122 public String phoneNumber; 123 /** 124 * phone label 125 */ 126 public String phoneLabel; 127 /** 128 * name of the contact 129 */ 130 public String name; 131 /** 132 * the contact id in the contacts people table 133 */ 134 public long person_id; 135 /** 136 * the presence icon resource id 137 */ 138 public int presenceResId; 139 /* 140 * custom presence 141 */ 142 public String presenceText; 143 /** 144 * Avatar image for this contact. 145 */ 146 public BitmapDrawable mAvatar; 147 148 /** 149 * If true, it indicates the CacheEntry has old info. We want to give the user of this 150 * class a chance to use the old info, as it can still be useful for displaying something 151 * rather than nothing in the UI. But this flag indicates that the CacheEntry needs to be 152 * updated. 153 */ 154 private boolean isStale; 155 156 /** 157 * Returns true if this CacheEntry needs to be updated. However, cache may still contain 158 * the old information. 159 * 160 */ isStale()161 public boolean isStale() { 162 return isStale; 163 } 164 165 @Override toString()166 public String toString() { 167 StringBuilder buf = new StringBuilder("name=" + name); 168 buf.append(", phone=" + phoneNumber); 169 buf.append(", pid=" + person_id); 170 buf.append(", presence=" + presenceResId); 171 buf.append(", stale=" + isStale); 172 return buf.toString(); 173 } 174 }; 175 ContactInfoCache(Context context)176 private ContactInfoCache(Context context) { 177 mContext = context; 178 } 179 180 /** 181 * invalidates the cache entries by marking CacheEntry.isStale to true. 182 */ invalidateCache()183 public void invalidateCache() { 184 synchronized (mCache) { 185 for (Map.Entry<String, CacheEntry> e: mCache.entrySet()) { 186 CacheEntry entry = e.getValue(); 187 entry.isStale = true; 188 } 189 } 190 } 191 192 /** 193 * invalidates a single cache entry. Can pass in an email or number. 194 */ invalidateContact(String emailOrNumber)195 public void invalidateContact(String emailOrNumber) { 196 synchronized (mCache) { 197 CacheEntry entry = mCache.get(emailOrNumber); 198 if (entry != null) { 199 entry.isStale = true; 200 } 201 } 202 } 203 204 /** 205 * Initialize the global instance. Should call only once. 206 */ init(Context context)207 public static void init(Context context) { 208 sInstance = new ContactInfoCache(context); 209 } 210 211 /** 212 * Get the global instance. 213 */ getInstance()214 public static ContactInfoCache getInstance() { 215 return sInstance; 216 } 217 dump()218 public void dump() { 219 synchronized (mCache) { 220 Log.i(TAG, "ContactInfoCache.dump"); 221 222 for (String name : mCache.keySet()) { 223 CacheEntry entry = mCache.get(name); 224 if (entry != null) { 225 Log.i(TAG, "key=" + name + ", cacheEntry={" + entry.toString() + '}'); 226 } else { 227 Log.i(TAG, "key=" + name + ", cacheEntry={null}"); 228 } 229 } 230 } 231 } 232 233 /** 234 * Returns the caller info in CacheEntry. 235 */ getContactInfo(String numberOrEmail, boolean allowQuery)236 public CacheEntry getContactInfo(String numberOrEmail, boolean allowQuery) { 237 if (Mms.isEmailAddress(numberOrEmail)) { 238 return getContactInfoForEmailAddress(numberOrEmail, allowQuery); 239 } else { 240 return getContactInfoForPhoneNumber(numberOrEmail, allowQuery); 241 } 242 } 243 getContactInfo(String numberOrEmail)244 public CacheEntry getContactInfo(String numberOrEmail) { 245 return getContactInfo(numberOrEmail, true); 246 } 247 248 /** 249 * Returns the caller info in a CacheEntry. If 'noQuery' is set to true, then this 250 * method only checks in the cache and makes no content provider query. 251 * 252 * @param number the phone number for the contact. 253 * @param allowQuery allow (potentially blocking) query the content provider if true. 254 * @return the CacheEntry containing the contact info. 255 */ getContactInfoForPhoneNumber(String number, boolean allowQuery)256 public CacheEntry getContactInfoForPhoneNumber(String number, boolean allowQuery) { 257 // TODO: numbers like "6501234567" and "+16501234567" are equivalent. 258 // we should convert them into a uniform format so that we don't cache 259 // them twice. 260 number = PhoneNumberUtils.stripSeparators(number); 261 synchronized (mCache) { 262 if (mCache.containsKey(number)) { 263 CacheEntry entry = mCache.get(number); 264 if (LOCAL_DEBUG) { 265 log("getContactInfo: number=" + number + ", name=" + entry.name + 266 ", presence=" + entry.presenceResId); 267 } 268 if (!allowQuery || !entry.isStale()) { 269 return entry; 270 } 271 } else if (!allowQuery) { 272 return null; 273 } 274 } 275 CacheEntry entry = queryContactInfoByNumber(number); 276 synchronized (mCache) { 277 mCache.put(number, entry); 278 } 279 return entry; 280 } 281 282 /** 283 * Queries the caller id info with the phone number. 284 * @return a CacheEntry containing the caller id info corresponding to the number. 285 */ queryContactInfoByNumber(String number)286 private CacheEntry queryContactInfoByNumber(String number) { 287 CacheEntry entry = new CacheEntry(); 288 entry.phoneNumber = number; 289 290 //if (LOCAL_DEBUG) log("queryContactInfoByNumber: number=" + number); 291 292 mContactInfoSelectionArgs[0] = number; 293 294 Cursor cursor = mContext.getContentResolver().query( 295 PHONES_WITH_PRESENCE_URI, 296 CALLER_ID_PROJECTION, 297 CALLER_ID_SELECTION, 298 mContactInfoSelectionArgs, 299 null); 300 301 if (cursor == null) { 302 Log.w(TAG, "queryContactInfoByNumber(" + number + ") returned NULL cursor!" + 303 " contact uri used " + PHONES_WITH_PRESENCE_URI); 304 return entry; 305 } 306 307 try { 308 if (cursor.moveToFirst()) { 309 entry.phoneLabel = cursor.getString(PHONE_LABEL_COLUMN); 310 entry.name = cursor.getString(CONTACT_NAME_COLUMN); 311 entry.person_id = cursor.getLong(CONTACT_ID_COLUMN); 312 entry.presenceResId = getPresenceIconResourceId( 313 cursor.getInt(CONTACT_PRESENCE_COLUMN)); 314 entry.presenceText = cursor.getString(CONTACT_STATUS_COLUMN); 315 if (LOCAL_DEBUG) { 316 log("queryContactInfoByNumber: name=" + entry.name + ", number=" + number + 317 ", presence=" + entry.presenceResId); 318 } 319 320 loadAvatar(entry, cursor); 321 } 322 } finally { 323 cursor.close(); 324 } 325 326 return entry; 327 } 328 loadAvatar(CacheEntry entry, Cursor cursor)329 private void loadAvatar(CacheEntry entry, Cursor cursor) { 330 if (entry.person_id == 0 || entry.mAvatar != null) { 331 return; 332 } 333 334 Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, entry.person_id); 335 336 InputStream avatarDataStream = 337 Contacts.openContactPhotoInputStream( 338 mContext.getContentResolver(), 339 contactUri); 340 if (avatarDataStream != null) { 341 Bitmap b = BitmapFactory.decodeStream(avatarDataStream); 342 343 BitmapDrawable bd = 344 new BitmapDrawable(mContext.getResources(), b); 345 346 entry.mAvatar = bd; 347 try { 348 avatarDataStream.close(); 349 } catch (IOException e) { 350 entry.mAvatar = null; 351 } 352 } 353 } 354 355 /** 356 * Get the display names of contacts. Contacts can be either email address or 357 * phone number. 358 * 359 * @param address the addresses to lookup, separated by ";" 360 * @return a nicely formatted version of the contact names to display 361 */ getContactName(String address)362 public String getContactName(String address) { 363 if (TextUtils.isEmpty(address)) { 364 return ""; 365 } 366 367 StringBuilder result = new StringBuilder(); 368 for (String value : address.split(SEPARATOR)) { 369 if (value.length() > 0) { 370 result.append(SEPARATOR); 371 if (MessageUtils.isLocalNumber(value)) { 372 result.append(mContext.getString(com.android.internal.R.string.me)); 373 } else if (Mms.isEmailAddress(value)) { 374 result.append(getDisplayName(value)); 375 } else { 376 result.append(getCallerId(value)); 377 } 378 } 379 } 380 381 if (result.length() > 0) { 382 // Skip the first ";" 383 return result.substring(1); 384 } 385 386 return ""; 387 } 388 389 /** 390 * Get the display name of an email address. If the address already contains 391 * the name, parse and return it. Otherwise, query the contact database. Cache 392 * query results for repeated queries. 393 */ getDisplayName(String email)394 public String getDisplayName(String email) { 395 Matcher match = Mms.NAME_ADDR_EMAIL_PATTERN.matcher(email); 396 if (match.matches()) { 397 // email has display name 398 return getEmailDisplayName(match.group(1)); 399 } 400 401 CacheEntry entry = getContactInfoForEmailAddress(email, true /* allow query */); 402 if (entry != null && entry.name != null) { 403 return entry.name; 404 } 405 406 return email; 407 } 408 409 /** 410 * Returns the contact info for a given email address 411 * 412 * @param email the email address. 413 * @param allowQuery allow making (potentially blocking) content provider queries if true. 414 * @return a CacheEntry if the contact is found. 415 */ getContactInfoForEmailAddress(String email, boolean allowQuery)416 public CacheEntry getContactInfoForEmailAddress(String email, boolean allowQuery) { 417 synchronized (mCache) { 418 if (mCache.containsKey(email)) { 419 CacheEntry entry = mCache.get(email); 420 if (!allowQuery || !entry.isStale()) { 421 return entry; 422 } 423 } else if (!allowQuery) { 424 return null; 425 } 426 } 427 CacheEntry entry = queryEmailDisplayName(email); 428 synchronized (mCache) { 429 mCache.put(email, entry); 430 431 return entry; 432 } 433 } 434 435 /** 436 * A cached version of CallerInfo.getCallerId(). 437 */ getCallerId(String number)438 private String getCallerId(String number) { 439 ContactInfoCache.CacheEntry entry = getContactInfo(number); 440 if (entry != null && !TextUtils.isEmpty(entry.name)) { 441 return entry.name; 442 } 443 return number; 444 } 445 getEmailDisplayName(String displayString)446 private static String getEmailDisplayName(String displayString) { 447 Matcher match = Mms.QUOTED_STRING_PATTERN.matcher(displayString); 448 if (match.matches()) { 449 return match.group(1); 450 } 451 452 return displayString; 453 } 454 getPresenceIconResourceId(int presence)455 private int getPresenceIconResourceId(int presence) { 456 if (presence != Presence.OFFLINE) { 457 return Presence.getPresenceIconResourceId(presence); 458 } 459 460 return 0; 461 } 462 463 /** 464 * Query the contact email table to get the name of an email address. 465 */ queryEmailDisplayName(String email)466 private CacheEntry queryEmailDisplayName(String email) { 467 CacheEntry entry = new CacheEntry(); 468 469 mContactInfoSelectionArgs[0] = email; 470 471 Cursor cursor = SqliteWrapper.query(mContext, mContext.getContentResolver(), 472 EMAIL_WITH_PRESENCE_URI, 473 EMAIL_PROJECTION, 474 EMAIL_SELECTION, 475 mContactInfoSelectionArgs, 476 null); 477 478 if (cursor != null) { 479 try { 480 while (cursor.moveToNext()) { 481 entry.presenceResId = getPresenceIconResourceId( 482 cursor.getInt(EMAIL_STATUS_COLUMN)); 483 entry.person_id = cursor.getLong(EMAIL_ID_COLUMN); 484 485 String name = cursor.getString(EMAIL_NAME_COLUMN); 486 if (TextUtils.isEmpty(name)) { 487 name = cursor.getString(EMAIL_CONTACT_NAME_COLUMN); 488 } 489 if (!TextUtils.isEmpty(name)) { 490 entry.name = name; 491 loadAvatar(entry, cursor); 492 if (LOCAL_DEBUG) { 493 log("queryEmailDisplayName: name=" + entry.name + ", email=" + email + 494 ", presence=" + entry.presenceResId); 495 } 496 break; 497 } 498 499 } 500 } finally { 501 cursor.close(); 502 } 503 } 504 return entry; 505 } 506 log(String msg)507 private void log(String msg) { 508 Log.d(TAG, "[ContactInfoCache] " + msg); 509 } 510 } 511