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