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