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