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