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