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