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