* Queries the appropriate content provider for the contact associated with the number.
   * <p>Upon completion it also updates the cache in the call log, if it is different from {@code
   * callLogInfo}.
   * <p>The number might be either a SIP address or a phone number.
   * <p>It returns true if it updated the content of the cache and we should therefore tell the view
   * to update its content.
  private boolean queryContactInfo(String number, String countryIso, ContactInfo callLogInfo) {
    final ContactInfo info = mContactInfoHelper.lookupNumber(number, countryIso);

    if (info == null) {
      // The lookup failed, just return without requesting to update the view.
      return false;

    // Check the existing entry in the cache: only if it has changed we should update the
    // view.
    NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
    ContactInfo existingInfo = mContactInfoCache.getPossiblyExpired(numberCountryIso);
    final boolean isRemoteSource = info.sourceType != 0;

    // Don't force redraw if existing info in the cache is equal to {@link ContactInfo#EMPTY}
    // to avoid updating the data set for every new row that is scrolled into view.
    // see (https://googleplex-android-review.git.corp.google.com/#/c/166680/)

    // Exception: Photo uris for contacts from remote sources are not cached in the call log
    // cache, so we have to force a redraw for these contacts regardless.
    boolean updated =
        (existingInfo != ContactInfo.EMPTY || isRemoteSource) && !info.equals(existingInfo);

    // Store the data in the cache so that the UI thread can use to display it. Store it
    // even if it has not changed so that it is marked as not expired.
    mContactInfoCache.put(numberCountryIso, info);
    mCb.updateContactInfo(number, countryIso, info, callLogInfo);
    return updated;
  * Get the number from the Contacts, if available, since sometimes
  * the number provided by caller id may not be formatted properly
  * depending on the carrier (roaming) in use at the time of the
  * incoming call.
  * Logic : If the caller-id number starts with a "+", use it
  *         Else if the number in the contacts starts with a "+", use that one
  *         Else if the number in the contacts is longer, use that one
 public String getBetterNumberFromContacts(String number, String countryIso) {
   String matchingNumber = null;
   // Look in the cache first. If it's not found then query the Phones db
   NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
   ContactInfo ci = mContactInfoCache.getPossiblyExpired(numberCountryIso);
   if (ci != null && ci != ContactInfo.EMPTY) {
     matchingNumber = ci.number;
   } else {
     try {
       Cursor phonesCursor =
                   Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number),
       if (phonesCursor != null) {
         if (phonesCursor.moveToFirst()) {
           matchingNumber = phonesCursor.getString(PhoneQuery.MATCHED_NUMBER);
     } catch (Exception e) {
       // Use the number from the call log
   if (!TextUtils.isEmpty(matchingNumber)
       && (matchingNumber.startsWith("+") || matchingNumber.length() > number.length())) {
     number = matchingNumber;
   return number;
  public void invalidateCache() {

    // Restart the request-processing thread after the next draw.
  public ContactInfo lookupContact(
      String number, int numberPresentation, String countryIso, ContactInfo cachedContactInfo) {
    NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
    ExpirableCache.CachedValue<ContactInfo> cachedInfo =
    ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue();
    if (!PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation)
        || new PhoneNumberUtilsWrapper().isVoicemailNumber(number)) {
      // If this is a number that cannot be dialed, there is no point in looking up a contact
      // for it.
      info = ContactInfo.EMPTY;
    } else if (cachedInfo == null) {
      mContactInfoCache.put(numberCountryIso, ContactInfo.EMPTY);
      // Use the cached contact info from the call log.
      info = cachedContactInfo;
      // The db request should happen on a non-UI thread.
      // Request the contact details immediately since they are currently missing.
      enqueueRequest(number, countryIso, cachedContactInfo, true);
      // We will format the phone number when we make the background request.
    } else {
      if (cachedInfo.isExpired()) {
        // The contact info is no longer up to date, we should request it. However, we
        // do not need to request them immediately.
        enqueueRequest(number, countryIso, cachedContactInfo, false);
      } else if (!callLogInfoMatches(cachedContactInfo, info)) {
        // The call log information does not match the one we have, look it up again.
        // We could simply update the call log directly, but that needs to be done in a
        // background thread, so it is easier to simply request a new lookup, which will, as
        // a side-effect, update the call log.
        enqueueRequest(number, countryIso, cachedContactInfo, false);

      if (info == ContactInfo.EMPTY) {
        // Use the cached contact info from the call log.
        info = cachedContactInfo;

    return info;
  public CallLogAdapterHelper(
      Context context,
      Callback cb,
      ContactInfoHelper contactInfoHelper,
      PhoneNumberDisplayHelper phoneNumberHelper) {
    mContext = context;
    mCb = cb;
    mContactInfoHelper = contactInfoHelper;
    mPhoneNumberDisplayHelper = phoneNumberHelper;

    mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE);
    mRequests = new LinkedList<ContactInfoRequest>();
 void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) {
   NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
   mContactInfoCache.put(numberCountryIso, contactInfo);