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
17package com.android.incallui;
18
19import android.content.Context;
20import android.graphics.Bitmap;
21import android.graphics.drawable.BitmapDrawable;
22import android.graphics.drawable.Drawable;
23import android.media.RingtoneManager;
24import android.net.Uri;
25import android.os.Build.VERSION;
26import android.os.Build.VERSION_CODES;
27import android.os.SystemClock;
28import android.provider.ContactsContract.CommonDataKinds.Phone;
29import android.provider.ContactsContract.Contacts;
30import android.provider.ContactsContract.DisplayNameSources;
31import android.support.annotation.AnyThread;
32import android.support.annotation.MainThread;
33import android.support.annotation.NonNull;
34import android.support.annotation.Nullable;
35import android.support.annotation.WorkerThread;
36import android.support.v4.content.ContextCompat;
37import android.support.v4.os.UserManagerCompat;
38import android.telecom.TelecomManager;
39import android.telephony.PhoneNumberUtils;
40import android.text.TextUtils;
41import android.util.ArrayMap;
42import android.util.ArraySet;
43import com.android.contacts.common.ContactsUtils;
44import com.android.dialer.common.Assert;
45import com.android.dialer.common.concurrent.DialerExecutor;
46import com.android.dialer.common.concurrent.DialerExecutor.Worker;
47import com.android.dialer.common.concurrent.DialerExecutors;
48import com.android.dialer.logging.ContactLookupResult;
49import com.android.dialer.logging.ContactSource;
50import com.android.dialer.oem.CequintCallerIdManager;
51import com.android.dialer.oem.CequintCallerIdManager.CequintCallerIdContact;
52import com.android.dialer.phonenumbercache.CachedNumberLookupService;
53import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo;
54import com.android.dialer.phonenumbercache.ContactInfo;
55import com.android.dialer.phonenumbercache.PhoneNumberCache;
56import com.android.dialer.phonenumberutil.PhoneNumberHelper;
57import com.android.dialer.util.MoreStrings;
58import com.android.incallui.CallerInfoAsyncQuery.OnQueryCompleteListener;
59import com.android.incallui.ContactsAsyncHelper.OnImageLoadCompleteListener;
60import com.android.incallui.bindings.PhoneNumberService;
61import com.android.incallui.call.DialerCall;
62import com.android.incallui.incall.protocol.ContactPhotoType;
63import java.util.Map;
64import java.util.Objects;
65import java.util.Set;
66import java.util.concurrent.ConcurrentHashMap;
67import org.json.JSONException;
68import 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 */
76public 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
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
125  private ContactInfoCache(Context context) {
126    mContext = context;
127    mPhoneNumberService = Bindings.get(context).newPhoneNumberService(context);
128  }
129
130  public static synchronized ContactInfoCache getInstance(Context mContext) {
131    if (sCache == null) {
132      sCache = new ContactInfoCache(mContext.getApplicationContext());
133    }
134    return sCache;
135  }
136
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). */
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. */
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
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
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
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
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
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
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
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
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
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. */
600  public void clearCache() {
601    mInfoMap.clear();
602    mCallBacks.clear();
603    mQueryId = 0;
604  }
605
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
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
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
668  private void clearCallbacks(String callId) {
669    mCallBacks.remove(callId);
670  }
671
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
683    void onContactInfoComplete(String callId, ContactCacheEntry entry);
684
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
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
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
785    FindInfoCallback(boolean isIncoming, CallerInfoQueryToken queryToken) {
786      mIsIncoming = isIncoming;
787      mQueryToken = queryToken;
788    }
789
790    @Override
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
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
850    PhoneNumberServiceListener(String callId, int queryId) {
851      mCallId = callId;
852      mQueryIdOfRemoteLookup = queryId;
853    }
854
855    @Override
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
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
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
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. */
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