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 17 package com.android.incallui; 18 19 import android.content.Context; 20 import android.graphics.Bitmap; 21 import android.graphics.drawable.BitmapDrawable; 22 import android.graphics.drawable.Drawable; 23 import android.media.RingtoneManager; 24 import android.net.Uri; 25 import android.os.Build.VERSION; 26 import android.os.Build.VERSION_CODES; 27 import android.os.SystemClock; 28 import android.provider.ContactsContract.CommonDataKinds.Phone; 29 import android.provider.ContactsContract.Contacts; 30 import android.provider.ContactsContract.DisplayNameSources; 31 import android.support.annotation.AnyThread; 32 import android.support.annotation.MainThread; 33 import android.support.annotation.NonNull; 34 import android.support.annotation.Nullable; 35 import android.support.annotation.WorkerThread; 36 import android.support.v4.os.UserManagerCompat; 37 import android.telecom.TelecomManager; 38 import android.telephony.PhoneNumberUtils; 39 import android.text.TextUtils; 40 import android.util.ArrayMap; 41 import android.util.ArraySet; 42 import com.android.contacts.common.ContactsUtils; 43 import com.android.dialer.common.Assert; 44 import com.android.dialer.common.concurrent.DialerExecutor; 45 import com.android.dialer.common.concurrent.DialerExecutor.Worker; 46 import com.android.dialer.common.concurrent.DialerExecutors; 47 import com.android.dialer.logging.ContactLookupResult; 48 import com.android.dialer.logging.ContactSource; 49 import com.android.dialer.oem.CequintCallerIdManager; 50 import com.android.dialer.oem.CequintCallerIdManager.CequintCallerIdContact; 51 import com.android.dialer.phonenumbercache.CachedNumberLookupService; 52 import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo; 53 import com.android.dialer.phonenumbercache.ContactInfo; 54 import com.android.dialer.phonenumbercache.PhoneNumberCache; 55 import com.android.dialer.phonenumberutil.PhoneNumberHelper; 56 import com.android.dialer.util.MoreStrings; 57 import com.android.incallui.CallerInfoAsyncQuery.OnQueryCompleteListener; 58 import com.android.incallui.ContactsAsyncHelper.OnImageLoadCompleteListener; 59 import com.android.incallui.bindings.PhoneNumberService; 60 import com.android.incallui.call.DialerCall; 61 import com.android.incallui.incall.protocol.ContactPhotoType; 62 import java.util.Map; 63 import java.util.Objects; 64 import java.util.Set; 65 import java.util.concurrent.ConcurrentHashMap; 66 import org.json.JSONException; 67 import org.json.JSONObject; 68 69 /** 70 * Class responsible for querying Contact Information for DialerCall objects. Can perform 71 * asynchronous requests to the Contact Provider for information as well as respond synchronously 72 * for any data that it currently has cached from previous queries. This class always gets called 73 * from the UI thread so it does not need thread protection. 74 */ 75 public class ContactInfoCache implements OnImageLoadCompleteListener { 76 77 private static final String TAG = ContactInfoCache.class.getSimpleName(); 78 private static final int TOKEN_UPDATE_PHOTO_FOR_CALL_STATE = 0; 79 private static ContactInfoCache sCache = null; 80 private final Context mContext; 81 private final PhoneNumberService mPhoneNumberService; 82 // Cache info map needs to be thread-safe since it could be modified by both main thread and 83 // worker thread. 84 private final ConcurrentHashMap<String, ContactCacheEntry> mInfoMap = new ConcurrentHashMap<>(); 85 private final Map<String, Set<ContactInfoCacheCallback>> mCallBacks = new ArrayMap<>(); 86 private Drawable mDefaultContactPhotoDrawable; 87 private int mQueryId; 88 private final DialerExecutor<CnapInformationWrapper> cachedNumberLookupExecutor = 89 DialerExecutors.createNonUiTaskBuilder(new CachedNumberLookupWorker()).build(); 90 91 private static class CachedNumberLookupWorker implements Worker<CnapInformationWrapper, Void> { 92 @Nullable 93 @Override 94 public Void doInBackground(@Nullable CnapInformationWrapper input) { 95 if (input == null) { 96 return null; 97 } 98 ContactInfo contactInfo = new ContactInfo(); 99 CachedContactInfo cacheInfo = input.service.buildCachedContactInfo(contactInfo); 100 cacheInfo.setSource(ContactSource.Type.SOURCE_TYPE_CNAP, "CNAP", 0); 101 contactInfo.name = input.cnapName; 102 contactInfo.number = input.number; 103 try { 104 final JSONObject contactRows = 105 new JSONObject() 106 .put( 107 Phone.CONTENT_ITEM_TYPE, 108 new JSONObject().put(Phone.NUMBER, contactInfo.number)); 109 final String jsonString = 110 new JSONObject() 111 .put(Contacts.DISPLAY_NAME, contactInfo.name) 112 .put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME) 113 .put(Contacts.CONTENT_ITEM_TYPE, contactRows) 114 .toString(); 115 cacheInfo.setLookupKey(jsonString); 116 } catch (JSONException e) { 117 Log.w(TAG, "Creation of lookup key failed when caching CNAP information"); 118 } 119 input.service.addContact(input.context.getApplicationContext(), cacheInfo); 120 return null; 121 } 122 } 123 124 private ContactInfoCache(Context context) { 125 mContext = context; 126 mPhoneNumberService = Bindings.get(context).newPhoneNumberService(context); 127 } 128 129 public static synchronized ContactInfoCache getInstance(Context mContext) { 130 if (sCache == null) { 131 sCache = new ContactInfoCache(mContext.getApplicationContext()); 132 } 133 return sCache; 134 } 135 136 static ContactCacheEntry buildCacheEntryFromCall( 137 Context context, DialerCall call, boolean isIncoming) { 138 final ContactCacheEntry entry = new ContactCacheEntry(); 139 140 // TODO: get rid of caller info. 141 final CallerInfo info = CallerInfoUtils.buildCallerInfo(context, call); 142 ContactInfoCache.populateCacheEntry(context, info, entry, call.getNumberPresentation()); 143 return entry; 144 } 145 146 /** Populate a cache entry from a call (which got converted into a caller info). */ 147 private static void populateCacheEntry( 148 @NonNull Context context, 149 @NonNull CallerInfo info, 150 @NonNull ContactCacheEntry cce, 151 int presentation) { 152 Objects.requireNonNull(info); 153 String displayName = null; 154 String displayNumber = null; 155 String label = null; 156 boolean isSipCall = false; 157 158 // It appears that there is a small change in behaviour with the 159 // PhoneUtils' startGetCallerInfo whereby if we query with an 160 // empty number, we will get a valid CallerInfo object, but with 161 // fields that are all null, and the isTemporary boolean input 162 // parameter as true. 163 164 // In the past, we would see a NULL callerinfo object, but this 165 // ends up causing null pointer exceptions elsewhere down the 166 // line in other cases, so we need to make this fix instead. It 167 // appears that this was the ONLY call to PhoneUtils 168 // .getCallerInfo() that relied on a NULL CallerInfo to indicate 169 // an unknown contact. 170 171 // Currently, info.phoneNumber may actually be a SIP address, and 172 // if so, it might sometimes include the "sip:" prefix. That 173 // prefix isn't really useful to the user, though, so strip it off 174 // if present. (For any other URI scheme, though, leave the 175 // prefix alone.) 176 // TODO: It would be cleaner for CallerInfo to explicitly support 177 // SIP addresses instead of overloading the "phoneNumber" field. 178 // Then we could remove this hack, and instead ask the CallerInfo 179 // for a "user visible" form of the SIP address. 180 String number = info.phoneNumber; 181 182 if (!TextUtils.isEmpty(number)) { 183 isSipCall = PhoneNumberHelper.isUriNumber(number); 184 if (number.startsWith("sip:")) { 185 number = number.substring(4); 186 } 187 } 188 189 if (TextUtils.isEmpty(info.name)) { 190 // No valid "name" in the CallerInfo, so fall back to 191 // something else. 192 // (Typically, we promote the phone number up to the "name" slot 193 // onscreen, and possibly display a descriptive string in the 194 // "number" slot.) 195 if (TextUtils.isEmpty(number) && TextUtils.isEmpty(info.cnapName)) { 196 // No name *or* number! Display a generic "unknown" string 197 // (or potentially some other default based on the presentation.) 198 displayName = getPresentationString(context, presentation, info.callSubject); 199 Log.d(TAG, " ==> no name *or* number! displayName = " + displayName); 200 } else if (presentation != TelecomManager.PRESENTATION_ALLOWED) { 201 // This case should never happen since the network should never send a phone # 202 // AND a restricted presentation. However we leave it here in case of weird 203 // network behavior 204 displayName = getPresentationString(context, presentation, info.callSubject); 205 Log.d(TAG, " ==> presentation not allowed! displayName = " + displayName); 206 } else if (!TextUtils.isEmpty(info.cnapName)) { 207 // No name, but we do have a valid CNAP name, so use that. 208 displayName = info.cnapName; 209 info.name = info.cnapName; 210 displayNumber = PhoneNumberHelper.formatNumber(number, context); 211 Log.d( 212 TAG, 213 " ==> cnapName available: displayName '" 214 + displayName 215 + "', displayNumber '" 216 + displayNumber 217 + "'"); 218 } else { 219 // No name; all we have is a number. This is the typical 220 // case when an incoming call doesn't match any contact, 221 // or if you manually dial an outgoing number using the 222 // dialpad. 223 displayNumber = PhoneNumberHelper.formatNumber(number, context); 224 225 Log.d( 226 TAG, 227 " ==> no name; falling back to number:" 228 + " displayNumber '" 229 + Log.pii(displayNumber) 230 + "'"); 231 } 232 } else { 233 // We do have a valid "name" in the CallerInfo. Display that 234 // in the "name" slot, and the phone number in the "number" slot. 235 if (presentation != TelecomManager.PRESENTATION_ALLOWED) { 236 // This case should never happen since the network should never send a name 237 // AND a restricted presentation. However we leave it here in case of weird 238 // network behavior 239 displayName = getPresentationString(context, presentation, info.callSubject); 240 Log.d( 241 TAG, 242 " ==> valid name, but presentation not allowed!" + " displayName = " + displayName); 243 } else { 244 // Causes cce.namePrimary to be set as info.name below. CallCardPresenter will 245 // later determine whether to use the name or nameAlternative when presenting 246 displayName = info.name; 247 cce.nameAlternative = info.nameAlternative; 248 displayNumber = PhoneNumberHelper.formatNumber(number, context); 249 label = info.phoneLabel; 250 Log.d( 251 TAG, 252 " ==> name is present in CallerInfo: displayName '" 253 + displayName 254 + "', displayNumber '" 255 + displayNumber 256 + "'"); 257 } 258 } 259 260 cce.namePrimary = displayName; 261 cce.number = displayNumber; 262 cce.location = info.geoDescription; 263 cce.label = label; 264 cce.isSipCall = isSipCall; 265 cce.userType = info.userType; 266 cce.originalPhoneNumber = info.phoneNumber; 267 cce.shouldShowLocation = info.shouldShowGeoDescription; 268 269 if (info.contactExists) { 270 cce.contactLookupResult = ContactLookupResult.Type.LOCAL_CONTACT; 271 } 272 } 273 274 /** Gets name strings based on some special presentation modes and the associated custom label. */ 275 private static String getPresentationString( 276 Context context, int presentation, String customLabel) { 277 String name = context.getString(R.string.unknown); 278 if (!TextUtils.isEmpty(customLabel) 279 && ((presentation == TelecomManager.PRESENTATION_UNKNOWN) 280 || (presentation == TelecomManager.PRESENTATION_RESTRICTED))) { 281 name = customLabel; 282 return name; 283 } else { 284 if (presentation == TelecomManager.PRESENTATION_RESTRICTED) { 285 name = PhoneNumberHelper.getDisplayNameForRestrictedNumber(context).toString(); 286 } else if (presentation == TelecomManager.PRESENTATION_PAYPHONE) { 287 name = context.getString(R.string.payphone); 288 } 289 } 290 return name; 291 } 292 293 ContactCacheEntry getInfo(String callId) { 294 return mInfoMap.get(callId); 295 } 296 297 private static final class CnapInformationWrapper { 298 final String number; 299 final String cnapName; 300 final Context context; 301 final CachedNumberLookupService service; 302 303 CnapInformationWrapper( 304 String number, String cnapName, Context context, CachedNumberLookupService service) { 305 this.number = number; 306 this.cnapName = cnapName; 307 this.context = context; 308 this.service = service; 309 } 310 } 311 312 void maybeInsertCnapInformationIntoCache( 313 Context context, final DialerCall call, final CallerInfo info) { 314 final CachedNumberLookupService cachedNumberLookupService = 315 PhoneNumberCache.get(context).getCachedNumberLookupService(); 316 if (!UserManagerCompat.isUserUnlocked(context)) { 317 Log.i(TAG, "User locked, not inserting cnap info into cache"); 318 return; 319 } 320 if (cachedNumberLookupService == null 321 || TextUtils.isEmpty(info.cnapName) 322 || mInfoMap.get(call.getId()) != null) { 323 return; 324 } 325 Log.i(TAG, "Found contact with CNAP name - inserting into cache"); 326 327 cachedNumberLookupExecutor.executeParallel( 328 new CnapInformationWrapper( 329 call.getNumber(), info.cnapName, context, cachedNumberLookupService)); 330 } 331 332 /** 333 * Requests contact data for the DialerCall object passed in. Returns the data through callback. 334 * If callback is null, no response is made, however the query is still performed and cached. 335 * 336 * @param callback The function to call back when the call is found. Can be null. 337 */ 338 @MainThread 339 public void findInfo( 340 @NonNull final DialerCall call, 341 final boolean isIncoming, 342 @NonNull ContactInfoCacheCallback callback) { 343 Assert.isMainThread(); 344 Objects.requireNonNull(callback); 345 346 final String callId = call.getId(); 347 final ContactCacheEntry cacheEntry = mInfoMap.get(callId); 348 Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId); 349 350 // We need to force a new query if phone number has changed. 351 boolean forceQuery = needForceQuery(call, cacheEntry); 352 Log.d(TAG, "findInfo: callId = " + callId + "; forceQuery = " + forceQuery); 353 354 // If we have a previously obtained intermediate result return that now except needs 355 // force query. 356 if (cacheEntry != null && !forceQuery) { 357 Log.d( 358 TAG, 359 "Contact lookup. In memory cache hit; lookup " 360 + (callBacks == null ? "complete" : "still running")); 361 callback.onContactInfoComplete(callId, cacheEntry); 362 // If no other callbacks are in flight, we're done. 363 if (callBacks == null) { 364 return; 365 } 366 } 367 368 // If the entry already exists, add callback 369 if (callBacks != null) { 370 Log.d(TAG, "Another query is in progress, add callback only."); 371 callBacks.add(callback); 372 if (!forceQuery) { 373 Log.d(TAG, "No need to query again, just return and wait for existing query to finish"); 374 return; 375 } 376 } else { 377 Log.d(TAG, "Contact lookup. In memory cache miss; searching provider."); 378 // New lookup 379 callBacks = new ArraySet<>(); 380 callBacks.add(callback); 381 mCallBacks.put(callId, callBacks); 382 } 383 384 /** 385 * Performs a query for caller information. Save any immediate data we get from the query. An 386 * asynchronous query may also be made for any data that we do not already have. Some queries, 387 * such as those for voicemail and emergency call information, will not perform an additional 388 * asynchronous query. 389 */ 390 final CallerInfoQueryToken queryToken = new CallerInfoQueryToken(mQueryId, callId); 391 mQueryId++; 392 final CallerInfo callerInfo = 393 CallerInfoUtils.getCallerInfoForCall( 394 mContext, 395 call, 396 new DialerCallCookieWrapper(callId, call.getNumberPresentation(), call.getCnapName()), 397 new FindInfoCallback(isIncoming, queryToken)); 398 399 if (cacheEntry != null) { 400 // We should not override the old cache item until the new query is 401 // back. We should only update the queryId. Otherwise, we may see 402 // flicker of the name and image (old cache -> new cache before query 403 // -> new cache after query) 404 cacheEntry.queryId = queryToken.mQueryId; 405 Log.d(TAG, "There is an existing cache. Do not override until new query is back"); 406 } else { 407 ContactCacheEntry initialCacheEntry = 408 updateCallerInfoInCacheOnAnyThread( 409 callId, call.getNumberPresentation(), callerInfo, isIncoming, false, queryToken); 410 sendInfoNotifications(callId, initialCacheEntry); 411 } 412 } 413 414 @AnyThread 415 private ContactCacheEntry updateCallerInfoInCacheOnAnyThread( 416 String callId, 417 int numberPresentation, 418 CallerInfo callerInfo, 419 boolean isIncoming, 420 boolean didLocalLookup, 421 CallerInfoQueryToken queryToken) { 422 Log.d( 423 TAG, 424 "updateCallerInfoInCacheOnAnyThread: callId = " 425 + callId 426 + "; queryId = " 427 + queryToken.mQueryId 428 + "; didLocalLookup = " 429 + didLocalLookup); 430 431 int presentationMode = numberPresentation; 432 if (callerInfo.contactExists 433 || callerInfo.isEmergencyNumber() 434 || callerInfo.isVoiceMailNumber()) { 435 presentationMode = TelecomManager.PRESENTATION_ALLOWED; 436 } 437 438 // We always replace the entry. The only exception is the same photo case. 439 ContactCacheEntry cacheEntry = buildEntry(mContext, callerInfo, presentationMode); 440 cacheEntry.queryId = queryToken.mQueryId; 441 442 ContactCacheEntry existingCacheEntry = mInfoMap.get(callId); 443 Log.d(TAG, "Existing cacheEntry in hashMap " + existingCacheEntry); 444 445 if (didLocalLookup) { 446 // Before issuing a request for more data from other services, we only check that the 447 // contact wasn't found in the local DB. We don't check the if the cache entry already 448 // has a name because we allow overriding cnap data with data from other services. 449 if (!callerInfo.contactExists && mPhoneNumberService != null) { 450 Log.d(TAG, "Contact lookup. Local contacts miss, checking remote"); 451 final PhoneNumberServiceListener listener = 452 new PhoneNumberServiceListener(callId, queryToken.mQueryId); 453 cacheEntry.hasPendingQuery = true; 454 mPhoneNumberService.getPhoneNumberInfo(cacheEntry.number, listener, listener, isIncoming); 455 } else if (cacheEntry.displayPhotoUri != null) { 456 // When the difference between 2 numbers is only the prefix (e.g. + or IDD), 457 // we will still trigger force query so that the number can be updated on 458 // the calling screen. We need not query the image again if the previous 459 // query already has the image to avoid flickering. 460 if (existingCacheEntry != null 461 && existingCacheEntry.displayPhotoUri != null 462 && existingCacheEntry.displayPhotoUri.equals(cacheEntry.displayPhotoUri) 463 && existingCacheEntry.photo != null) { 464 Log.d(TAG, "Same picture. Do not need start image load."); 465 cacheEntry.photo = existingCacheEntry.photo; 466 cacheEntry.photoType = existingCacheEntry.photoType; 467 return cacheEntry; 468 } 469 470 Log.d(TAG, "Contact lookup. Local contact found, starting image load"); 471 // Load the image with a callback to update the image state. 472 // When the load is finished, onImageLoadComplete() will be called. 473 cacheEntry.hasPendingQuery = true; 474 ContactsAsyncHelper.startObtainPhotoAsync( 475 TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, 476 mContext, 477 cacheEntry.displayPhotoUri, 478 ContactInfoCache.this, 479 queryToken); 480 } 481 Log.d(TAG, "put entry into map: " + cacheEntry); 482 mInfoMap.put(callId, cacheEntry); 483 } else { 484 // Don't overwrite if there is existing cache. 485 Log.d(TAG, "put entry into map if not exists: " + cacheEntry); 486 mInfoMap.putIfAbsent(callId, cacheEntry); 487 } 488 return cacheEntry; 489 } 490 491 private void maybeUpdateFromCequintCallerId( 492 CallerInfo callerInfo, String cnapName, boolean isIncoming) { 493 if (!CequintCallerIdManager.isCequintCallerIdEnabled(mContext)) { 494 return; 495 } 496 if (callerInfo.phoneNumber == null) { 497 return; 498 } 499 CequintCallerIdContact cequintCallerIdContact = 500 CequintCallerIdManager.getCequintCallerIdContactForInCall( 501 mContext, callerInfo.phoneNumber, cnapName, isIncoming); 502 503 if (cequintCallerIdContact == null) { 504 return; 505 } 506 boolean hasUpdate = false; 507 508 if (TextUtils.isEmpty(callerInfo.name) && !TextUtils.isEmpty(cequintCallerIdContact.name)) { 509 callerInfo.name = cequintCallerIdContact.name; 510 hasUpdate = true; 511 } 512 if (!TextUtils.isEmpty(cequintCallerIdContact.geoDescription)) { 513 callerInfo.geoDescription = cequintCallerIdContact.geoDescription; 514 callerInfo.shouldShowGeoDescription = true; 515 hasUpdate = true; 516 } 517 // Don't overwrite photo in local contacts. 518 if (!callerInfo.contactExists 519 && callerInfo.contactDisplayPhotoUri == null 520 && cequintCallerIdContact.imageUrl != null) { 521 callerInfo.contactDisplayPhotoUri = Uri.parse(cequintCallerIdContact.imageUrl); 522 hasUpdate = true; 523 } 524 // Set contact to exist to avoid phone number service lookup. 525 callerInfo.contactExists = hasUpdate; 526 } 527 528 /** 529 * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface. Update contact photo 530 * when image is loaded in worker thread. 531 */ 532 @WorkerThread 533 @Override 534 public void onImageLoaded(int token, Drawable photo, Bitmap photoIcon, Object cookie) { 535 Assert.isWorkerThread(); 536 CallerInfoQueryToken myCookie = (CallerInfoQueryToken) cookie; 537 final String callId = myCookie.mCallId; 538 final int queryId = myCookie.mQueryId; 539 if (!isWaitingForThisQuery(callId, queryId)) { 540 return; 541 } 542 loadImage(photo, photoIcon, cookie); 543 } 544 545 private void loadImage(Drawable photo, Bitmap photoIcon, Object cookie) { 546 Log.d(TAG, "Image load complete with context: ", mContext); 547 // TODO: may be nice to update the image view again once the newer one 548 // is available on contacts database. 549 CallerInfoQueryToken myCookie = (CallerInfoQueryToken) cookie; 550 final String callId = myCookie.mCallId; 551 ContactCacheEntry entry = mInfoMap.get(callId); 552 553 if (entry == null) { 554 Log.e(TAG, "Image Load received for empty search entry."); 555 clearCallbacks(callId); 556 return; 557 } 558 559 Log.d(TAG, "setting photo for entry: ", entry); 560 561 // Conference call icons are being handled in CallCardPresenter. 562 if (photo != null) { 563 Log.v(TAG, "direct drawable: ", photo); 564 entry.photo = photo; 565 entry.photoType = ContactPhotoType.CONTACT; 566 } else if (photoIcon != null) { 567 Log.v(TAG, "photo icon: ", photoIcon); 568 entry.photo = new BitmapDrawable(mContext.getResources(), photoIcon); 569 entry.photoType = ContactPhotoType.CONTACT; 570 } else { 571 Log.v(TAG, "unknown photo"); 572 entry.photo = null; 573 entry.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER; 574 } 575 } 576 577 /** 578 * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface. make sure that the 579 * call state is reflected after the image is loaded. 580 */ 581 @MainThread 582 @Override 583 public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie) { 584 Assert.isMainThread(); 585 CallerInfoQueryToken myCookie = (CallerInfoQueryToken) cookie; 586 final String callId = myCookie.mCallId; 587 final int queryId = myCookie.mQueryId; 588 if (!isWaitingForThisQuery(callId, queryId)) { 589 return; 590 } 591 sendImageNotifications(callId, mInfoMap.get(callId)); 592 593 clearCallbacks(callId); 594 } 595 596 /** Blows away the stored cache values. */ 597 public void clearCache() { 598 mInfoMap.clear(); 599 mCallBacks.clear(); 600 mQueryId = 0; 601 } 602 603 private ContactCacheEntry buildEntry(Context context, CallerInfo info, int presentation) { 604 final ContactCacheEntry cce = new ContactCacheEntry(); 605 populateCacheEntry(context, info, cce, presentation); 606 607 // This will only be true for emergency numbers 608 if (info.photoResource != 0) { 609 cce.photo = context.getResources().getDrawable(info.photoResource); 610 } else if (info.isCachedPhotoCurrent) { 611 if (info.cachedPhoto != null) { 612 cce.photo = info.cachedPhoto; 613 cce.photoType = ContactPhotoType.CONTACT; 614 } else { 615 cce.photo = getDefaultContactPhotoDrawable(); 616 cce.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER; 617 } 618 } else { 619 cce.displayPhotoUri = info.contactDisplayPhotoUri; 620 cce.photo = null; 621 } 622 623 // Support any contact id in N because QuickContacts in N starts supporting enterprise 624 // contact id 625 if (info.lookupKeyOrNull != null 626 && (VERSION.SDK_INT >= VERSION_CODES.N || info.contactIdOrZero != 0)) { 627 cce.lookupUri = Contacts.getLookupUri(info.contactIdOrZero, info.lookupKeyOrNull); 628 } else { 629 Log.v(TAG, "lookup key is null or contact ID is 0 on M. Don't create a lookup uri."); 630 cce.lookupUri = null; 631 } 632 633 cce.lookupKey = info.lookupKeyOrNull; 634 cce.contactRingtoneUri = info.contactRingtoneUri; 635 if (cce.contactRingtoneUri == null || Uri.EMPTY.equals(cce.contactRingtoneUri)) { 636 cce.contactRingtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE); 637 } 638 639 return cce; 640 } 641 642 /** Sends the updated information to call the callbacks for the entry. */ 643 @MainThread 644 private void sendInfoNotifications(String callId, ContactCacheEntry entry) { 645 Assert.isMainThread(); 646 final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId); 647 if (callBacks != null) { 648 for (ContactInfoCacheCallback callBack : callBacks) { 649 callBack.onContactInfoComplete(callId, entry); 650 } 651 } 652 } 653 654 @MainThread 655 private void sendImageNotifications(String callId, ContactCacheEntry entry) { 656 Assert.isMainThread(); 657 final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId); 658 if (callBacks != null && entry.photo != null) { 659 for (ContactInfoCacheCallback callBack : callBacks) { 660 callBack.onImageLoadComplete(callId, entry); 661 } 662 } 663 } 664 665 private void clearCallbacks(String callId) { 666 mCallBacks.remove(callId); 667 } 668 669 public Drawable getDefaultContactPhotoDrawable() { 670 if (mDefaultContactPhotoDrawable == null) { 671 mDefaultContactPhotoDrawable = 672 mContext.getResources().getDrawable(R.drawable.img_no_image_automirrored); 673 } 674 return mDefaultContactPhotoDrawable; 675 } 676 677 /** Callback interface for the contact query. */ 678 public interface ContactInfoCacheCallback { 679 680 void onContactInfoComplete(String callId, ContactCacheEntry entry); 681 682 void onImageLoadComplete(String callId, ContactCacheEntry entry); 683 } 684 685 /** This is cached contact info, which should be the ONLY info used by UI. */ 686 public static class ContactCacheEntry { 687 688 public String namePrimary; 689 public String nameAlternative; 690 public String number; 691 public String location; 692 public String label; 693 public Drawable photo; 694 @ContactPhotoType int photoType; 695 boolean isSipCall; 696 // Note in cache entry whether this is a pending async loading action to know whether to 697 // wait for its callback or not. 698 boolean hasPendingQuery; 699 /** This will be used for the "view" notification. */ 700 public Uri contactUri; 701 /** Either a display photo or a thumbnail URI. */ 702 Uri displayPhotoUri; 703 704 public Uri lookupUri; // Sent to NotificationMananger 705 public String lookupKey; 706 public ContactLookupResult.Type contactLookupResult = ContactLookupResult.Type.NOT_FOUND; 707 public long userType = ContactsUtils.USER_TYPE_CURRENT; 708 Uri contactRingtoneUri; 709 /** Query id to identify the query session. */ 710 int queryId; 711 /** The phone number without any changes to display to the user (ex: cnap...) */ 712 String originalPhoneNumber; 713 boolean shouldShowLocation; 714 715 boolean isBusiness; 716 717 @Override 718 public String toString() { 719 return "ContactCacheEntry{" 720 + "name='" 721 + MoreStrings.toSafeString(namePrimary) 722 + '\'' 723 + ", nameAlternative='" 724 + MoreStrings.toSafeString(nameAlternative) 725 + '\'' 726 + ", number='" 727 + MoreStrings.toSafeString(number) 728 + '\'' 729 + ", location='" 730 + MoreStrings.toSafeString(location) 731 + '\'' 732 + ", label='" 733 + label 734 + '\'' 735 + ", photo=" 736 + photo 737 + ", isSipCall=" 738 + isSipCall 739 + ", contactUri=" 740 + contactUri 741 + ", displayPhotoUri=" 742 + displayPhotoUri 743 + ", contactLookupResult=" 744 + contactLookupResult 745 + ", userType=" 746 + userType 747 + ", contactRingtoneUri=" 748 + contactRingtoneUri 749 + ", queryId=" 750 + queryId 751 + ", originalPhoneNumber=" 752 + originalPhoneNumber 753 + ", shouldShowLocation=" 754 + shouldShowLocation 755 + '}'; 756 } 757 } 758 759 private static final class DialerCallCookieWrapper { 760 final String callId; 761 final int numberPresentation; 762 final String cnapName; 763 764 DialerCallCookieWrapper(String callId, int numberPresentation, String cnapName) { 765 this.callId = callId; 766 this.numberPresentation = numberPresentation; 767 this.cnapName = cnapName; 768 } 769 } 770 771 private class FindInfoCallback implements OnQueryCompleteListener { 772 773 private final boolean mIsIncoming; 774 private final CallerInfoQueryToken mQueryToken; 775 776 FindInfoCallback(boolean isIncoming, CallerInfoQueryToken queryToken) { 777 mIsIncoming = isIncoming; 778 mQueryToken = queryToken; 779 } 780 781 @Override 782 public void onDataLoaded(int token, Object cookie, CallerInfo ci) { 783 Assert.isWorkerThread(); 784 DialerCallCookieWrapper cw = (DialerCallCookieWrapper) cookie; 785 if (!isWaitingForThisQuery(cw.callId, mQueryToken.mQueryId)) { 786 return; 787 } 788 long start = SystemClock.uptimeMillis(); 789 maybeUpdateFromCequintCallerId(ci, cw.cnapName, mIsIncoming); 790 long time = SystemClock.uptimeMillis() - start; 791 Log.d(TAG, "Cequint Caller Id look up takes " + time + " ms."); 792 updateCallerInfoInCacheOnAnyThread(cw.callId, cw.numberPresentation, ci, mIsIncoming, true, mQueryToken); 793 } 794 795 @Override 796 public void onQueryComplete(int token, Object cookie, CallerInfo callerInfo) { 797 Assert.isMainThread(); 798 DialerCallCookieWrapper cw = (DialerCallCookieWrapper) cookie; 799 String callId = cw.callId; 800 if (!isWaitingForThisQuery(cw.callId, mQueryToken.mQueryId)) { 801 return; 802 } 803 ContactCacheEntry cacheEntry = mInfoMap.get(callId); 804 // This may happen only when InCallPresenter attempt to cleanup. 805 if (cacheEntry == null) { 806 Log.w(TAG, "Contact lookup done, but cache entry is not found."); 807 clearCallbacks(callId); 808 return; 809 } 810 sendInfoNotifications(callId, cacheEntry); 811 if (!cacheEntry.hasPendingQuery) { 812 if (callerInfo.contactExists) { 813 Log.d(TAG, "Contact lookup done. Local contact found, no image."); 814 } else { 815 Log.d( 816 TAG, 817 "Contact lookup done. Local contact not found and" 818 + " no remote lookup service available."); 819 } 820 clearCallbacks(callId); 821 } 822 } 823 } 824 825 class PhoneNumberServiceListener 826 implements PhoneNumberService.NumberLookupListener, PhoneNumberService.ImageLookupListener { 827 828 private final String mCallId; 829 private final int mQueryIdOfRemoteLookup; 830 831 PhoneNumberServiceListener(String callId, int queryId) { 832 mCallId = callId; 833 mQueryIdOfRemoteLookup = queryId; 834 } 835 836 @Override 837 public void onPhoneNumberInfoComplete(final PhoneNumberService.PhoneNumberInfo info) { 838 Log.d(TAG, "PhoneNumberServiceListener.onPhoneNumberInfoComplete"); 839 if (!isWaitingForThisQuery(mCallId, mQueryIdOfRemoteLookup)) { 840 return; 841 } 842 843 // If we got a miss, this is the end of the lookup pipeline, 844 // so clear the callbacks and return. 845 if (info == null) { 846 Log.d(TAG, "Contact lookup done. Remote contact not found."); 847 clearCallbacks(mCallId); 848 return; 849 } 850 ContactCacheEntry entry = new ContactCacheEntry(); 851 entry.namePrimary = info.getDisplayName(); 852 entry.number = info.getNumber(); 853 entry.contactLookupResult = info.getLookupSource(); 854 entry.isBusiness = info.isBusiness(); 855 final int type = info.getPhoneType(); 856 final String label = info.getPhoneLabel(); 857 if (type == Phone.TYPE_CUSTOM) { 858 entry.label = label; 859 } else { 860 final CharSequence typeStr = Phone.getTypeLabel(mContext.getResources(), type, label); 861 entry.label = typeStr == null ? null : typeStr.toString(); 862 } 863 final ContactCacheEntry oldEntry = mInfoMap.get(mCallId); 864 if (oldEntry != null) { 865 // Location is only obtained from local lookup so persist 866 // the value for remote lookups. Once we have a name this 867 // field is no longer used; it is persisted here in case 868 // the UI is ever changed to use it. 869 entry.location = oldEntry.location; 870 entry.shouldShowLocation = oldEntry.shouldShowLocation; 871 // Contact specific ringtone is obtained from local lookup. 872 entry.contactRingtoneUri = oldEntry.contactRingtoneUri; 873 } 874 875 // If no image and it's a business, switch to using the default business avatar. 876 if (info.getImageUrl() == null && info.isBusiness()) { 877 Log.d(TAG, "Business has no image. Using default."); 878 entry.photo = mContext.getResources().getDrawable(R.drawable.img_business); 879 entry.photoType = ContactPhotoType.BUSINESS; 880 } 881 882 Log.d(TAG, "put entry into map: " + entry); 883 mInfoMap.put(mCallId, entry); 884 sendInfoNotifications(mCallId, entry); 885 886 entry.hasPendingQuery = info.getImageUrl() != null; 887 888 // If there is no image then we should not expect another callback. 889 if (!entry.hasPendingQuery) { 890 // We're done, so clear callbacks 891 clearCallbacks(mCallId); 892 } 893 } 894 895 @Override 896 public void onImageFetchComplete(Bitmap bitmap) { 897 Log.d(TAG, "PhoneNumberServiceListener.onImageFetchComplete"); 898 if (!isWaitingForThisQuery(mCallId, mQueryIdOfRemoteLookup)) { 899 return; 900 } 901 CallerInfoQueryToken queryToken = new CallerInfoQueryToken(mQueryIdOfRemoteLookup, mCallId); 902 loadImage(null, bitmap, queryToken); 903 onImageLoadComplete(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, null, bitmap, queryToken); 904 } 905 } 906 907 private boolean needForceQuery(DialerCall call, ContactCacheEntry cacheEntry) { 908 if (call == null || call.isConferenceCall()) { 909 return false; 910 } 911 912 String newPhoneNumber = PhoneNumberUtils.stripSeparators(call.getNumber()); 913 if (cacheEntry == null) { 914 // No info in the map yet so it is the 1st query 915 Log.d(TAG, "needForceQuery: first query"); 916 return true; 917 } 918 String oldPhoneNumber = PhoneNumberUtils.stripSeparators(cacheEntry.originalPhoneNumber); 919 920 if (!TextUtils.equals(oldPhoneNumber, newPhoneNumber)) { 921 Log.d(TAG, "phone number has changed: " + oldPhoneNumber + " -> " + newPhoneNumber); 922 return true; 923 } 924 925 return false; 926 } 927 928 private static final class CallerInfoQueryToken { 929 final int mQueryId; 930 final String mCallId; 931 932 CallerInfoQueryToken(int queryId, String callId) { 933 mQueryId = queryId; 934 mCallId = callId; 935 } 936 } 937 938 /** Check if the queryId in the cached map is the same as the one from query result. */ 939 private boolean isWaitingForThisQuery(String callId, int queryId) { 940 final ContactCacheEntry existingCacheEntry = mInfoMap.get(callId); 941 if (existingCacheEntry == null) { 942 // This might happen if lookup on background thread comes back before the initial entry is 943 // created. 944 Log.d(TAG, "Cached entry is null."); 945 return true; 946 } else { 947 int waitingQueryId = existingCacheEntry.queryId; 948 Log.d(TAG, "waitingQueryId = " + waitingQueryId + "; queryId = " + queryId); 949 return waitingQueryId == queryId; 950 } 951 } 952 } 953