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