• 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.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
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     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 
getInstance(Context mContext)136   public static synchronized ContactInfoCache getInstance(Context mContext) {
137     if (cache == null) {
138       cache = new ContactInfoCache(mContext.getApplicationContext());
139     }
140     return cache;
141   }
142 
buildCacheEntryFromCall( Context context, DialerCall call, boolean isIncoming)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). */
populateCacheEntry( @onNull Context context, @NonNull CallerInfo info, @NonNull ContactCacheEntry cce, int presentation)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. */
getPresentationString( Context context, int presentation, String customLabel)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 
getInfo(String callId)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 
CnapInformationWrapper( String number, String cnapName, Context context, CachedNumberLookupService service)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 
maybeInsertCnapInformationIntoCache( Context context, final DialerCall call, final CallerInfo info)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
findInfo( @onNull final DialerCall call, final boolean isIncoming, @NonNull ContactInfoCacheCallback callback)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
updateCallerInfoInCacheOnAnyThread( String callId, int numberPresentation, CallerInfo callerInfo, boolean didLocalLookup, CallerInfoQueryToken queryToken)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 
maybeUpdateFromCequintCallerId( CallerInfo callerInfo, String cnapName, boolean isIncoming)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
onImageLoaded(int token, Drawable photo, Bitmap photoIcon, Object cookie)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 
loadImage(Drawable photo, Bitmap photoIcon, Object cookie)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
onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie)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. */
clearCache()618   public void clearCache() {
619     infoMap.clear();
620     callBacks.clear();
621     queryId = 0;
622   }
623 
buildEntry(Context context, CallerInfo info, int presentation)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
sendInfoNotifications(String callId, ContactCacheEntry entry)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
sendImageNotifications(String callId, ContactCacheEntry entry)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 
clearCallbacks(String callId)689   private void clearCallbacks(String callId) {
690     callBacks.remove(callId);
691   }
692 
693   /** Callback interface for the contact query. */
694   public interface ContactInfoCacheCallback {
695 
onContactInfoComplete(String callId, ContactCacheEntry entry)696     void onContactInfoComplete(String callId, ContactCacheEntry entry);
697 
onImageLoadComplete(String callId, ContactCacheEntry entry)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 
isLocalContact()734     public boolean isLocalContact() {
735       return contactLookupResult == ContactLookupResult.Type.LOCAL_CONTACT;
736     }
737 
738     @Override
toString()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 
DialerCallCookieWrapper(String callId, int numberPresentation, String cnapName)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 
FindInfoCallback(boolean isIncoming, CallerInfoQueryToken queryToken)799     FindInfoCallback(boolean isIncoming, CallerInfoQueryToken queryToken) {
800       this.isIncoming = isIncoming;
801       this.queryToken = queryToken;
802     }
803 
804     @Override
onDataLoaded(int token, Object cookie, CallerInfo ci)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
onQueryComplete(int token, Object cookie, CallerInfo callerInfo)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 
PhoneNumberServiceListener(String callId, int queryId)868     PhoneNumberServiceListener(String callId, int queryId) {
869       this.callId = callId;
870       queryIdOfRemoteLookup = queryId;
871     }
872 
873     @Override
onPhoneNumberInfoComplete(final PhoneNumberService.PhoneNumberInfo info)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
onImageFetchComplete(Bitmap bitmap)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 
needForceQuery(DialerCall call, ContactCacheEntry cacheEntry)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 
CallerInfoQueryToken(int queryId, String callId)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. */
isWaitingForThisQuery(String callId, int queryId)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