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