1/* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17package com.android.incallui; 18 19import android.content.Context; 20import android.graphics.Bitmap; 21import android.graphics.drawable.BitmapDrawable; 22import android.graphics.drawable.Drawable; 23import android.net.Uri; 24import android.os.AsyncTask; 25import android.os.Looper; 26import android.provider.ContactsContract; 27import android.provider.ContactsContract.Contacts; 28import android.provider.ContactsContract.DisplayNameSources; 29import android.provider.ContactsContract.CommonDataKinds.Phone; 30import android.telecom.TelecomManager; 31import android.text.TextUtils; 32 33import com.android.contacts.common.util.PhoneNumberHelper; 34import com.android.dialer.calllog.ContactInfo; 35import com.android.dialer.service.CachedNumberLookupService; 36import com.android.dialer.service.CachedNumberLookupService.CachedContactInfo; 37import com.android.incallui.service.PhoneNumberService; 38import com.android.incalluibind.ObjectFactory; 39import com.android.services.telephony.common.MoreStrings; 40 41import org.json.JSONException; 42import org.json.JSONObject; 43 44import com.google.common.collect.Maps; 45import com.google.common.collect.Sets; 46import com.google.common.base.Objects; 47import com.google.common.base.Preconditions; 48 49import java.util.HashMap; 50import java.util.Set; 51 52/** 53 * Class responsible for querying Contact Information for Call objects. Can perform asynchronous 54 * requests to the Contact Provider for information as well as respond synchronously for any data 55 * that it currently has cached from previous queries. This class always gets called from the UI 56 * thread so it does not need thread protection. 57 */ 58public class ContactInfoCache implements ContactsAsyncHelper.OnImageLoadCompleteListener { 59 60 private static final String TAG = ContactInfoCache.class.getSimpleName(); 61 private static final int TOKEN_UPDATE_PHOTO_FOR_CALL_STATE = 0; 62 63 private final Context mContext; 64 private final PhoneNumberService mPhoneNumberService; 65 private final CachedNumberLookupService mCachedNumberLookupService; 66 private final HashMap<String, ContactCacheEntry> mInfoMap = Maps.newHashMap(); 67 private final HashMap<String, Set<ContactInfoCacheCallback>> mCallBacks = Maps.newHashMap(); 68 69 private static ContactInfoCache sCache = null; 70 71 private Drawable mDefaultContactPhotoDrawable; 72 private Drawable mConferencePhotoDrawable; 73 74 public static synchronized ContactInfoCache getInstance(Context mContext) { 75 if (sCache == null) { 76 sCache = new ContactInfoCache(mContext.getApplicationContext()); 77 } 78 return sCache; 79 } 80 81 private ContactInfoCache(Context context) { 82 mContext = context; 83 mPhoneNumberService = ObjectFactory.newPhoneNumberService(context); 84 mCachedNumberLookupService = 85 com.android.dialerbind.ObjectFactory.newCachedNumberLookupService(); 86 } 87 88 public ContactCacheEntry getInfo(String callId) { 89 return mInfoMap.get(callId); 90 } 91 92 public static ContactCacheEntry buildCacheEntryFromCall(Context context, Call call, 93 boolean isIncoming) { 94 final ContactCacheEntry entry = new ContactCacheEntry(); 95 96 // TODO: get rid of caller info. 97 final CallerInfo info = CallerInfoUtils.buildCallerInfo(context, call); 98 ContactInfoCache.populateCacheEntry(context, info, entry, call.getNumberPresentation(), 99 isIncoming); 100 return entry; 101 } 102 103 public void maybeInsertCnapInformationIntoCache(Context context, final Call call, 104 final CallerInfo info) { 105 if (mCachedNumberLookupService == null || TextUtils.isEmpty(info.cnapName) 106 || mInfoMap.get(call.getId()) != null) { 107 return; 108 } 109 final Context applicationContext = context.getApplicationContext(); 110 Log.i(TAG, "Found contact with CNAP name - inserting into cache"); 111 new AsyncTask<Void, Void, Void>() { 112 @Override 113 protected Void doInBackground(Void... params) { 114 ContactInfo contactInfo = new ContactInfo(); 115 CachedContactInfo cacheInfo = mCachedNumberLookupService.buildCachedContactInfo( 116 contactInfo); 117 cacheInfo.setSource(CachedContactInfo.SOURCE_TYPE_CNAP, "CNAP", 0); 118 contactInfo.name = info.cnapName; 119 contactInfo.number = call.getNumber(); 120 contactInfo.type = ContactsContract.CommonDataKinds.Phone.TYPE_MAIN; 121 try { 122 final JSONObject contactRows = new JSONObject().put(Phone.CONTENT_ITEM_TYPE, 123 new JSONObject() 124 .put(Phone.NUMBER, contactInfo.number) 125 .put(Phone.TYPE, Phone.TYPE_MAIN)); 126 final String jsonString = new JSONObject() 127 .put(Contacts.DISPLAY_NAME, contactInfo.name) 128 .put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME) 129 .put(Contacts.CONTENT_ITEM_TYPE, contactRows).toString(); 130 cacheInfo.setLookupKey(jsonString); 131 } catch (JSONException e) { 132 Log.w(TAG, "Creation of lookup key failed when caching CNAP information"); 133 } 134 mCachedNumberLookupService.addContact(applicationContext, cacheInfo); 135 return null; 136 } 137 }.execute(); 138 } 139 140 private class FindInfoCallback implements CallerInfoAsyncQuery.OnQueryCompleteListener { 141 private final boolean mIsIncoming; 142 143 public FindInfoCallback(boolean isIncoming) { 144 mIsIncoming = isIncoming; 145 } 146 147 @Override 148 public void onQueryComplete(int token, Object cookie, CallerInfo callerInfo) { 149 findInfoQueryComplete((Call) cookie, callerInfo, mIsIncoming, true); 150 } 151 } 152 153 /** 154 * Requests contact data for the Call object passed in. 155 * Returns the data through callback. If callback is null, no response is made, however the 156 * query is still performed and cached. 157 * 158 * @param callback The function to call back when the call is found. Can be null. 159 */ 160 public void findInfo(final Call call, final boolean isIncoming, 161 ContactInfoCacheCallback callback) { 162 Preconditions.checkState(Looper.getMainLooper().getThread() == Thread.currentThread()); 163 Preconditions.checkNotNull(callback); 164 165 final String callId = call.getId(); 166 final ContactCacheEntry cacheEntry = mInfoMap.get(callId); 167 Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId); 168 169 // If we have a previously obtained intermediate result return that now 170 if (cacheEntry != null) { 171 Log.d(TAG, "Contact lookup. In memory cache hit; lookup " 172 + (callBacks == null ? "complete" : "still running")); 173 callback.onContactInfoComplete(callId, cacheEntry); 174 // If no other callbacks are in flight, we're done. 175 if (callBacks == null) { 176 return; 177 } 178 } 179 180 // If the entry already exists, add callback 181 if (callBacks != null) { 182 callBacks.add(callback); 183 return; 184 } 185 Log.d(TAG, "Contact lookup. In memory cache miss; searching provider."); 186 // New lookup 187 callBacks = Sets.newHashSet(); 188 callBacks.add(callback); 189 mCallBacks.put(callId, callBacks); 190 191 /** 192 * Performs a query for caller information. 193 * Save any immediate data we get from the query. An asynchronous query may also be made 194 * for any data that we do not already have. Some queries, such as those for voicemail and 195 * emergency call information, will not perform an additional asynchronous query. 196 */ 197 final CallerInfo callerInfo = CallerInfoUtils.getCallerInfoForCall( 198 mContext, call, new FindInfoCallback(isIncoming)); 199 200 findInfoQueryComplete(call, callerInfo, isIncoming, false); 201 } 202 203 private void findInfoQueryComplete(Call call, CallerInfo callerInfo, boolean isIncoming, 204 boolean didLocalLookup) { 205 final String callId = call.getId(); 206 int presentationMode = call.getNumberPresentation(); 207 if (callerInfo.contactExists || callerInfo.isEmergencyNumber() || 208 callerInfo.isVoiceMailNumber()) { 209 presentationMode = TelecomManager.PRESENTATION_ALLOWED; 210 } 211 212 ContactCacheEntry cacheEntry = mInfoMap.get(callId); 213 // Ensure we always have a cacheEntry. Replace the existing entry if 214 // it has no name or if we found a local contact. 215 if (cacheEntry == null || TextUtils.isEmpty(cacheEntry.name) || 216 callerInfo.contactExists) { 217 cacheEntry = buildEntry(mContext, callId, callerInfo, presentationMode, isIncoming); 218 mInfoMap.put(callId, cacheEntry); 219 } 220 221 sendInfoNotifications(callId, cacheEntry); 222 223 if (didLocalLookup) { 224 // Before issuing a request for more data from other services, we only check that the 225 // contact wasn't found in the local DB. We don't check the if the cache entry already 226 // has a name because we allow overriding cnap data with data from other services. 227 if (!callerInfo.contactExists && mPhoneNumberService != null) { 228 Log.d(TAG, "Contact lookup. Local contacts miss, checking remote"); 229 final PhoneNumberServiceListener listener = new PhoneNumberServiceListener(callId); 230 mPhoneNumberService.getPhoneNumberInfo(cacheEntry.number, listener, listener, 231 isIncoming); 232 } else if (cacheEntry.displayPhotoUri != null) { 233 Log.d(TAG, "Contact lookup. Local contact found, starting image load"); 234 // Load the image with a callback to update the image state. 235 // When the load is finished, onImageLoadComplete() will be called. 236 ContactsAsyncHelper.startObtainPhotoAsync(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, 237 mContext, cacheEntry.displayPhotoUri, ContactInfoCache.this, callId); 238 } else { 239 if (callerInfo.contactExists) { 240 Log.d(TAG, "Contact lookup done. Local contact found, no image."); 241 } else { 242 Log.d(TAG, "Contact lookup done. Local contact not found and" 243 + " no remote lookup service available."); 244 } 245 clearCallbacks(callId); 246 } 247 } 248 } 249 250 class PhoneNumberServiceListener implements PhoneNumberService.NumberLookupListener, 251 PhoneNumberService.ImageLookupListener { 252 private final String mCallId; 253 254 PhoneNumberServiceListener(String callId) { 255 mCallId = callId; 256 } 257 258 @Override 259 public void onPhoneNumberInfoComplete( 260 final PhoneNumberService.PhoneNumberInfo info) { 261 // If we got a miss, this is the end of the lookup pipeline, 262 // so clear the callbacks and return. 263 if (info == null) { 264 Log.d(TAG, "Contact lookup done. Remote contact not found."); 265 clearCallbacks(mCallId); 266 return; 267 } 268 269 ContactCacheEntry entry = new ContactCacheEntry(); 270 entry.name = info.getDisplayName(); 271 entry.number = info.getNumber(); 272 final int type = info.getPhoneType(); 273 final String label = info.getPhoneLabel(); 274 if (type == Phone.TYPE_CUSTOM) { 275 entry.label = label; 276 } else { 277 final CharSequence typeStr = Phone.getTypeLabel( 278 mContext.getResources(), type, label); 279 entry.label = typeStr == null ? null : typeStr.toString(); 280 } 281 final ContactCacheEntry oldEntry = mInfoMap.get(mCallId); 282 if (oldEntry != null) { 283 // Location is only obtained from local lookup so persist 284 // the value for remote lookups. Once we have a name this 285 // field is no longer used; it is persisted here in case 286 // the UI is ever changed to use it. 287 entry.location = oldEntry.location; 288 } 289 290 // If no image and it's a business, switch to using the default business avatar. 291 if (info.getImageUrl() == null && info.isBusiness()) { 292 Log.d(TAG, "Business has no image. Using default."); 293 entry.photo = mContext.getResources().getDrawable(R.drawable.img_business); 294 } 295 296 // Add the contact info to the cache. 297 mInfoMap.put(mCallId, entry); 298 sendInfoNotifications(mCallId, entry); 299 300 // If there is no image then we should not expect another callback. 301 if (info.getImageUrl() == null) { 302 // We're done, so clear callbacks 303 clearCallbacks(mCallId); 304 } 305 } 306 307 @Override 308 public void onImageFetchComplete(Bitmap bitmap) { 309 onImageLoadComplete(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, null, bitmap, mCallId); 310 } 311 } 312 313 /** 314 * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface. 315 * make sure that the call state is reflected after the image is loaded. 316 */ 317 @Override 318 public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie) { 319 Log.d(this, "Image load complete with context: ", mContext); 320 // TODO: may be nice to update the image view again once the newer one 321 // is available on contacts database. 322 323 final String callId = (String) cookie; 324 final ContactCacheEntry entry = mInfoMap.get(callId); 325 326 if (entry == null) { 327 Log.e(this, "Image Load received for empty search entry."); 328 clearCallbacks(callId); 329 return; 330 } 331 Log.d(this, "setting photo for entry: ", entry); 332 333 // Conference call icons are being handled in CallCardPresenter. 334 if (photo != null) { 335 Log.v(this, "direct drawable: ", photo); 336 entry.photo = photo; 337 } else if (photoIcon != null) { 338 Log.v(this, "photo icon: ", photoIcon); 339 entry.photo = new BitmapDrawable(mContext.getResources(), photoIcon); 340 } else { 341 Log.v(this, "unknown photo"); 342 entry.photo = null; 343 } 344 345 sendImageNotifications(callId, entry); 346 clearCallbacks(callId); 347 } 348 349 /** 350 * Blows away the stored cache values. 351 */ 352 public void clearCache() { 353 mInfoMap.clear(); 354 mCallBacks.clear(); 355 } 356 357 private ContactCacheEntry buildEntry(Context context, String callId, 358 CallerInfo info, int presentation, boolean isIncoming) { 359 // The actual strings we're going to display onscreen: 360 Drawable photo = null; 361 362 final ContactCacheEntry cce = new ContactCacheEntry(); 363 populateCacheEntry(context, info, cce, presentation, isIncoming); 364 365 // This will only be true for emergency numbers 366 if (info.photoResource != 0) { 367 photo = context.getResources().getDrawable(info.photoResource); 368 } else if (info.isCachedPhotoCurrent) { 369 if (info.cachedPhoto != null) { 370 photo = info.cachedPhoto; 371 } else { 372 photo = getDefaultContactPhotoDrawable(); 373 } 374 } else if (info.contactDisplayPhotoUri == null) { 375 photo = getDefaultContactPhotoDrawable(); 376 } else { 377 cce.displayPhotoUri = info.contactDisplayPhotoUri; 378 } 379 380 if (info.lookupKeyOrNull == null || info.contactIdOrZero == 0) { 381 Log.v(TAG, "lookup key is null or contact ID is 0. Don't create a lookup uri."); 382 cce.lookupUri = null; 383 } else { 384 cce.lookupUri = Contacts.getLookupUri(info.contactIdOrZero, info.lookupKeyOrNull); 385 } 386 387 cce.photo = photo; 388 cce.lookupKey = info.lookupKeyOrNull; 389 390 return cce; 391 } 392 393 /** 394 * Populate a cache entry from a call (which got converted into a caller info). 395 */ 396 public static void populateCacheEntry(Context context, CallerInfo info, ContactCacheEntry cce, 397 int presentation, boolean isIncoming) { 398 Preconditions.checkNotNull(info); 399 String displayName = null; 400 String displayNumber = null; 401 String displayLocation = null; 402 String label = null; 403 boolean isSipCall = false; 404 405 // It appears that there is a small change in behaviour with the 406 // PhoneUtils' startGetCallerInfo whereby if we query with an 407 // empty number, we will get a valid CallerInfo object, but with 408 // fields that are all null, and the isTemporary boolean input 409 // parameter as true. 410 411 // In the past, we would see a NULL callerinfo object, but this 412 // ends up causing null pointer exceptions elsewhere down the 413 // line in other cases, so we need to make this fix instead. It 414 // appears that this was the ONLY call to PhoneUtils 415 // .getCallerInfo() that relied on a NULL CallerInfo to indicate 416 // an unknown contact. 417 418 // Currently, infi.phoneNumber may actually be a SIP address, and 419 // if so, it might sometimes include the "sip:" prefix. That 420 // prefix isn't really useful to the user, though, so strip it off 421 // if present. (For any other URI scheme, though, leave the 422 // prefix alone.) 423 // TODO: It would be cleaner for CallerInfo to explicitly support 424 // SIP addresses instead of overloading the "phoneNumber" field. 425 // Then we could remove this hack, and instead ask the CallerInfo 426 // for a "user visible" form of the SIP address. 427 String number = info.phoneNumber; 428 429 if (!TextUtils.isEmpty(number)) { 430 isSipCall = PhoneNumberHelper.isUriNumber(number); 431 if (number.startsWith("sip:")) { 432 number = number.substring(4); 433 } 434 } 435 436 if (TextUtils.isEmpty(info.name)) { 437 // No valid "name" in the CallerInfo, so fall back to 438 // something else. 439 // (Typically, we promote the phone number up to the "name" slot 440 // onscreen, and possibly display a descriptive string in the 441 // "number" slot.) 442 if (TextUtils.isEmpty(number)) { 443 // No name *or* number! Display a generic "unknown" string 444 // (or potentially some other default based on the presentation.) 445 displayName = getPresentationString(context, presentation); 446 Log.d(TAG, " ==> no name *or* number! displayName = " + displayName); 447 } else if (presentation != TelecomManager.PRESENTATION_ALLOWED) { 448 // This case should never happen since the network should never send a phone # 449 // AND a restricted presentation. However we leave it here in case of weird 450 // network behavior 451 displayName = getPresentationString(context, presentation); 452 Log.d(TAG, " ==> presentation not allowed! displayName = " + displayName); 453 } else if (!TextUtils.isEmpty(info.cnapName)) { 454 // No name, but we do have a valid CNAP name, so use that. 455 displayName = info.cnapName; 456 info.name = info.cnapName; 457 displayNumber = number; 458 Log.d(TAG, " ==> cnapName available: displayName '" + displayName + 459 "', displayNumber '" + displayNumber + "'"); 460 } else { 461 // No name; all we have is a number. This is the typical 462 // case when an incoming call doesn't match any contact, 463 // or if you manually dial an outgoing number using the 464 // dialpad. 465 displayNumber = number; 466 467 // Display a geographical description string if available 468 // (but only for incoming calls.) 469 if (isIncoming) { 470 // TODO (CallerInfoAsyncQuery cleanup): Fix the CallerInfo 471 // query to only do the geoDescription lookup in the first 472 // place for incoming calls. 473 displayLocation = info.geoDescription; // may be null 474 Log.d(TAG, "Geodescrption: " + info.geoDescription); 475 } 476 477 Log.d(TAG, " ==> no name; falling back to number:" 478 + " displayNumber '" + Log.pii(displayNumber) 479 + "', displayLocation '" + displayLocation + "'"); 480 } 481 } else { 482 // We do have a valid "name" in the CallerInfo. Display that 483 // in the "name" slot, and the phone number in the "number" slot. 484 if (presentation != TelecomManager.PRESENTATION_ALLOWED) { 485 // This case should never happen since the network should never send a name 486 // AND a restricted presentation. However we leave it here in case of weird 487 // network behavior 488 displayName = getPresentationString(context, presentation); 489 Log.d(TAG, " ==> valid name, but presentation not allowed!" + 490 " displayName = " + displayName); 491 } else { 492 displayName = info.name; 493 displayNumber = number; 494 label = info.phoneLabel; 495 Log.d(TAG, " ==> name is present in CallerInfo: displayName '" + displayName 496 + "', displayNumber '" + displayNumber + "'"); 497 } 498 } 499 500 cce.name = displayName; 501 cce.number = displayNumber; 502 cce.location = displayLocation; 503 cce.label = label; 504 cce.isSipCall = isSipCall; 505 } 506 507 /** 508 * Sends the updated information to call the callbacks for the entry. 509 */ 510 private void sendInfoNotifications(String callId, ContactCacheEntry entry) { 511 final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId); 512 if (callBacks != null) { 513 for (ContactInfoCacheCallback callBack : callBacks) { 514 callBack.onContactInfoComplete(callId, entry); 515 } 516 } 517 } 518 519 private void sendImageNotifications(String callId, ContactCacheEntry entry) { 520 final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId); 521 if (callBacks != null && entry.photo != null) { 522 for (ContactInfoCacheCallback callBack : callBacks) { 523 callBack.onImageLoadComplete(callId, entry); 524 } 525 } 526 } 527 528 private void clearCallbacks(String callId) { 529 mCallBacks.remove(callId); 530 } 531 532 /** 533 * Gets name strings based on some special presentation modes. 534 */ 535 private static String getPresentationString(Context context, int presentation) { 536 String name = context.getString(R.string.unknown); 537 if (presentation == TelecomManager.PRESENTATION_RESTRICTED) { 538 name = context.getString(R.string.private_num); 539 } else if (presentation == TelecomManager.PRESENTATION_PAYPHONE) { 540 name = context.getString(R.string.payphone); 541 } 542 return name; 543 } 544 545 public Drawable getDefaultContactPhotoDrawable() { 546 if (mDefaultContactPhotoDrawable == null) { 547 mDefaultContactPhotoDrawable = 548 mContext.getResources().getDrawable(R.drawable.img_no_image_automirrored); 549 } 550 return mDefaultContactPhotoDrawable; 551 } 552 553 public Drawable getConferenceDrawable() { 554 if (mConferencePhotoDrawable == null) { 555 mConferencePhotoDrawable = 556 mContext.getResources().getDrawable(R.drawable.img_conference_automirrored); 557 } 558 return mConferencePhotoDrawable; 559 } 560 561 /** 562 * Callback interface for the contact query. 563 */ 564 public interface ContactInfoCacheCallback { 565 public void onContactInfoComplete(String callId, ContactCacheEntry entry); 566 public void onImageLoadComplete(String callId, ContactCacheEntry entry); 567 } 568 569 public static class ContactCacheEntry { 570 public String name; 571 public String number; 572 public String location; 573 public String label; 574 public Drawable photo; 575 public boolean isSipCall; 576 /** This will be used for the "view" notification. */ 577 public Uri contactUri; 578 /** Either a display photo or a thumbnail URI. */ 579 public Uri displayPhotoUri; 580 public Uri lookupUri; // Sent to NotificationMananger 581 public String lookupKey; 582 583 @Override 584 public String toString() { 585 return Objects.toStringHelper(this) 586 .add("name", MoreStrings.toSafeString(name)) 587 .add("number", MoreStrings.toSafeString(number)) 588 .add("location", MoreStrings.toSafeString(location)) 589 .add("label", label) 590 .add("photo", photo) 591 .add("isSipCall", isSipCall) 592 .add("contactUri", contactUri) 593 .add("displayPhotoUri", displayPhotoUri) 594 .toString(); 595 } 596 } 597} 598