Home | History | Annotate | Download | only in incallui
      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
     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 
    125   private ContactInfoCache(Context context) {
    126     mContext = context;
    127     mPhoneNumberService = Bindings.get(context).newPhoneNumberService(context);
    128   }
    129 
    130   public static synchronized ContactInfoCache getInstance(Context mContext) {
    131     if (sCache == null) {
    132       sCache = new ContactInfoCache(mContext.getApplicationContext());
    133     }
    134     return sCache;
    135   }
    136 
    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). */
    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. */
    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 
    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 
    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 
    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
    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
    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 
    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
    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 
    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
    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. */
    600   public void clearCache() {
    601     mInfoMap.clear();
    602     mCallBacks.clear();
    603     mQueryId = 0;
    604   }
    605 
    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
    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
    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 
    668   private void clearCallbacks(String callId) {
    669     mCallBacks.remove(callId);
    670   }
    671 
    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 
    683     void onContactInfoComplete(String callId, ContactCacheEntry entry);
    684 
    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
    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 
    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 
    785     FindInfoCallback(boolean isIncoming, CallerInfoQueryToken queryToken) {
    786       mIsIncoming = isIncoming;
    787       mQueryToken = queryToken;
    788     }
    789 
    790     @Override
    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
    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 
    850     PhoneNumberServiceListener(String callId, int queryId) {
    851       mCallId = callId;
    852       mQueryIdOfRemoteLookup = queryId;
    853     }
    854 
    855     @Override
    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
    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 
    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 
    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. */
    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