Home | History | Annotate | Download | only in phonenumbercache
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
      5  * in compliance with the License. You may obtain a copy of the License at
      6  *
      7  * http://www.apache.org/licenses/LICENSE-2.0
      8  *
      9  * Unless required by applicable law or agreed to in writing, software distributed under the License
     10  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
     11  * or implied. See the License for the specific language governing permissions and limitations under
     12  * the License.
     13  */
     14 
     15 package com.android.dialer.phonenumbercache;
     16 
     17 import android.annotation.TargetApi;
     18 import android.content.ContentResolver;
     19 import android.content.ContentValues;
     20 import android.content.Context;
     21 import android.database.Cursor;
     22 import android.database.sqlite.SQLiteFullException;
     23 import android.net.Uri;
     24 import android.os.Build.VERSION;
     25 import android.os.Build.VERSION_CODES;
     26 import android.provider.CallLog.Calls;
     27 import android.provider.ContactsContract;
     28 import android.provider.ContactsContract.CommonDataKinds.Phone;
     29 import android.provider.ContactsContract.Contacts;
     30 import android.provider.ContactsContract.Directory;
     31 import android.provider.ContactsContract.DisplayNameSources;
     32 import android.provider.ContactsContract.PhoneLookup;
     33 import android.support.annotation.Nullable;
     34 import android.support.annotation.WorkerThread;
     35 import android.telephony.PhoneNumberUtils;
     36 import android.text.TextUtils;
     37 import com.android.contacts.common.ContactsUtils;
     38 import com.android.contacts.common.ContactsUtils.UserType;
     39 import com.android.contacts.common.compat.DirectoryCompat;
     40 import com.android.contacts.common.util.Constants;
     41 import com.android.contacts.common.util.UriUtils;
     42 import com.android.dialer.common.Assert;
     43 import com.android.dialer.common.LogUtil;
     44 import com.android.dialer.logging.ContactSource;
     45 import com.android.dialer.oem.CequintCallerIdManager;
     46 import com.android.dialer.oem.CequintCallerIdManager.CequintCallerIdContact;
     47 import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo;
     48 import com.android.dialer.phonenumberutil.PhoneNumberHelper;
     49 import com.android.dialer.telecom.TelecomUtil;
     50 import com.android.dialer.util.PermissionsUtil;
     51 import java.util.ArrayList;
     52 import java.util.List;
     53 import org.json.JSONException;
     54 import org.json.JSONObject;
     55 
     56 /** Utility class to look up the contact information for a given number. */
     57 // This class uses Java 7 language features, so it must target M+
     58 @TargetApi(VERSION_CODES.M)
     59 public class ContactInfoHelper {
     60 
     61   private static final String TAG = ContactInfoHelper.class.getSimpleName();
     62 
     63   private final Context mContext;
     64   private final String mCurrentCountryIso;
     65   private final CachedNumberLookupService mCachedNumberLookupService;
     66 
     67   public ContactInfoHelper(Context context, String currentCountryIso) {
     68     mContext = context;
     69     mCurrentCountryIso = currentCountryIso;
     70     mCachedNumberLookupService = PhoneNumberCache.get(mContext).getCachedNumberLookupService();
     71   }
     72 
     73   /**
     74    * Creates a JSON-encoded lookup uri for a unknown number without an associated contact
     75    *
     76    * @param number - Unknown phone number
     77    * @return JSON-encoded URI that can be used to perform a lookup when clicking on the quick
     78    *     contact card.
     79    */
     80   private static Uri createTemporaryContactUri(String number) {
     81     try {
     82       final JSONObject contactRows =
     83           new JSONObject()
     84               .put(
     85                   Phone.CONTENT_ITEM_TYPE,
     86                   new JSONObject().put(Phone.NUMBER, number).put(Phone.TYPE, Phone.TYPE_CUSTOM));
     87 
     88       final String jsonString =
     89           new JSONObject()
     90               .put(Contacts.DISPLAY_NAME, number)
     91               .put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.PHONE)
     92               .put(Contacts.CONTENT_ITEM_TYPE, contactRows)
     93               .toString();
     94 
     95       return Contacts.CONTENT_LOOKUP_URI
     96           .buildUpon()
     97           .appendPath(Constants.LOOKUP_URI_ENCODED)
     98           .appendQueryParameter(
     99               ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Long.MAX_VALUE))
    100           .encodedFragment(jsonString)
    101           .build();
    102     } catch (JSONException e) {
    103       return null;
    104     }
    105   }
    106 
    107   public static String lookUpDisplayNameAlternative(
    108       Context context, String lookupKey, @UserType long userType, @Nullable Long directoryId) {
    109     // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed.
    110     if (lookupKey == null || userType == ContactsUtils.USER_TYPE_WORK) {
    111       return null;
    112     }
    113 
    114     if (directoryId != null) {
    115       // Query {@link Contacts#CONTENT_LOOKUP_URI} with work lookup key is not allowed.
    116       if (DirectoryCompat.isEnterpriseDirectoryId(directoryId)) {
    117         return null;
    118       }
    119 
    120       // Skip this to avoid an extra remote network call for alternative name
    121       if (DirectoryCompat.isRemoteDirectoryId(directoryId)) {
    122         return null;
    123       }
    124     }
    125 
    126     final Uri uri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey);
    127     Cursor cursor = null;
    128     try {
    129       cursor =
    130           context
    131               .getContentResolver()
    132               .query(uri, PhoneQuery.DISPLAY_NAME_ALTERNATIVE_PROJECTION, null, null, null);
    133 
    134       if (cursor != null && cursor.moveToFirst()) {
    135         return cursor.getString(PhoneQuery.NAME_ALTERNATIVE);
    136       }
    137     } catch (IllegalArgumentException e) {
    138       // Avoid dialer crash when lookup key is not valid
    139       LogUtil.e(TAG, "IllegalArgumentException in lookUpDisplayNameAlternative", e);
    140     } finally {
    141       if (cursor != null) {
    142         cursor.close();
    143       }
    144     }
    145 
    146     return null;
    147   }
    148 
    149   public static Uri getContactInfoLookupUri(String number) {
    150     return getContactInfoLookupUri(number, -1);
    151   }
    152 
    153   public static Uri getContactInfoLookupUri(String number, long directoryId) {
    154     // Get URI for the number in the PhoneLookup table, with a parameter to indicate whether
    155     // the number is a SIP number.
    156     Uri uri = PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI;
    157     if (VERSION.SDK_INT < VERSION_CODES.N) {
    158       if (directoryId != -1) {
    159         // ENTERPRISE_CONTENT_FILTER_URI in M doesn't support directory lookup
    160         uri = PhoneLookup.CONTENT_FILTER_URI;
    161       } else {
    162         // b/25900607 in M. PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, encodes twice.
    163         number = Uri.encode(number);
    164       }
    165     }
    166     Uri.Builder builder =
    167         uri.buildUpon()
    168             .appendPath(number)
    169             .appendQueryParameter(
    170                 PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS,
    171                 String.valueOf(PhoneNumberHelper.isUriNumber(number)));
    172     if (directoryId != -1) {
    173       builder.appendQueryParameter(
    174           ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId));
    175     }
    176     return builder.build();
    177   }
    178 
    179   /**
    180    * Returns the contact information stored in an entry of the call log.
    181    *
    182    * @param c A cursor pointing to an entry in the call log.
    183    */
    184   public static ContactInfo getContactInfo(Cursor c) {
    185     ContactInfo info = new ContactInfo();
    186     info.lookupUri = UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_LOOKUP_URI));
    187     info.name = c.getString(CallLogQuery.CACHED_NAME);
    188     info.type = c.getInt(CallLogQuery.CACHED_NUMBER_TYPE);
    189     info.label = c.getString(CallLogQuery.CACHED_NUMBER_LABEL);
    190     String matchedNumber = c.getString(CallLogQuery.CACHED_MATCHED_NUMBER);
    191     String postDialDigits =
    192         (VERSION.SDK_INT >= VERSION_CODES.N) ? c.getString(CallLogQuery.POST_DIAL_DIGITS) : "";
    193     info.number =
    194         (matchedNumber == null) ? c.getString(CallLogQuery.NUMBER) + postDialDigits : matchedNumber;
    195 
    196     info.normalizedNumber = c.getString(CallLogQuery.CACHED_NORMALIZED_NUMBER);
    197     info.photoId = c.getLong(CallLogQuery.CACHED_PHOTO_ID);
    198     info.photoUri =
    199         UriUtils.nullForNonContactsUri(
    200             UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_PHOTO_URI)));
    201     info.formattedNumber = c.getString(CallLogQuery.CACHED_FORMATTED_NUMBER);
    202 
    203     return info;
    204   }
    205 
    206   @Nullable
    207   public ContactInfo lookupNumber(String number, String countryIso) {
    208     return lookupNumber(number, countryIso, -1);
    209   }
    210 
    211   /**
    212    * Returns the contact information for the given number.
    213    *
    214    * <p>If the number does not match any contact, returns a contact info containing only the number
    215    * and the formatted number.
    216    *
    217    * <p>If an error occurs during the lookup, it returns null.
    218    *
    219    * @param number the number to look up
    220    * @param countryIso the country associated with this number
    221    * @param directoryId the id of the directory to lookup
    222    */
    223   @Nullable
    224   @SuppressWarnings("ReferenceEquality")
    225   public ContactInfo lookupNumber(String number, String countryIso, long directoryId) {
    226     if (TextUtils.isEmpty(number)) {
    227       LogUtil.d("ContactInfoHelper.lookupNumber", "number is empty");
    228       return null;
    229     }
    230 
    231     ContactInfo info;
    232 
    233     if (PhoneNumberHelper.isUriNumber(number)) {
    234       LogUtil.d("ContactInfoHelper.lookupNumber", "number is sip");
    235       // The number is a SIP address..
    236       info = lookupContactFromUri(getContactInfoLookupUri(number, directoryId));
    237       if (info == null || info == ContactInfo.EMPTY) {
    238         // If lookup failed, check if the "username" of the SIP address is a phone number.
    239         String username = PhoneNumberHelper.getUsernameFromUriNumber(number);
    240         if (PhoneNumberUtils.isGlobalPhoneNumber(username)) {
    241           info = queryContactInfoForPhoneNumber(username, countryIso, directoryId);
    242         }
    243       }
    244     } else {
    245       // Look for a contact that has the given phone number.
    246       info = queryContactInfoForPhoneNumber(number, countryIso, directoryId);
    247     }
    248 
    249     final ContactInfo updatedInfo;
    250     if (info == null) {
    251       // The lookup failed.
    252       LogUtil.d("ContactInfoHelper.lookupNumber", "lookup failed");
    253       updatedInfo = null;
    254     } else {
    255       // If we did not find a matching contact, generate an empty contact info for the number.
    256       if (info == ContactInfo.EMPTY) {
    257         // Did not find a matching contact.
    258         updatedInfo = createEmptyContactInfoForNumber(number, countryIso);
    259       } else {
    260         updatedInfo = info;
    261       }
    262     }
    263     return updatedInfo;
    264   }
    265 
    266   private ContactInfo createEmptyContactInfoForNumber(String number, String countryIso) {
    267     ContactInfo contactInfo = new ContactInfo();
    268     contactInfo.number = number;
    269     contactInfo.formattedNumber = formatPhoneNumber(number, null, countryIso);
    270     contactInfo.normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso);
    271     contactInfo.lookupUri = createTemporaryContactUri(contactInfo.formattedNumber);
    272     return contactInfo;
    273   }
    274 
    275   /**
    276    * Return the contact info object if the remote directory lookup succeeds, otherwise return an
    277    * empty contact info for the number.
    278    */
    279   public ContactInfo lookupNumberInRemoteDirectory(String number, String countryIso) {
    280     if (mCachedNumberLookupService != null) {
    281       List<Long> remoteDirectories = getRemoteDirectories(mContext);
    282       for (long directoryId : remoteDirectories) {
    283         ContactInfo contactInfo = lookupNumber(number, countryIso, directoryId);
    284         if (hasName(contactInfo)) {
    285           return contactInfo;
    286         }
    287       }
    288     }
    289     return createEmptyContactInfoForNumber(number, countryIso);
    290   }
    291 
    292   public boolean hasName(ContactInfo contactInfo) {
    293     return contactInfo != null && !TextUtils.isEmpty(contactInfo.name);
    294   }
    295 
    296   private List<Long> getRemoteDirectories(Context context) {
    297     List<Long> remoteDirectories = new ArrayList<>();
    298     Uri uri =
    299         VERSION.SDK_INT >= VERSION_CODES.N
    300             ? Directory.ENTERPRISE_CONTENT_URI
    301             : Directory.CONTENT_URI;
    302     ContentResolver cr = context.getContentResolver();
    303     Cursor cursor = cr.query(uri, new String[] {Directory._ID}, null, null, null);
    304     int idIndex = cursor.getColumnIndex(Directory._ID);
    305     if (cursor == null) {
    306       return remoteDirectories;
    307     }
    308     try {
    309       while (cursor.moveToNext()) {
    310         long directoryId = cursor.getLong(idIndex);
    311         if (DirectoryCompat.isRemoteDirectoryId(directoryId)) {
    312           remoteDirectories.add(directoryId);
    313         }
    314       }
    315     } finally {
    316       cursor.close();
    317     }
    318     return remoteDirectories;
    319   }
    320 
    321   /**
    322    * Looks up a contact using the given URI.
    323    *
    324    * <p>It returns null if an error occurs, {@link ContactInfo#EMPTY} if no matching contact is
    325    * found, or the {@link ContactInfo} for the given contact.
    326    *
    327    * <p>The {@link ContactInfo#formattedNumber} field is always set to {@code null} in the returned
    328    * value.
    329    */
    330   ContactInfo lookupContactFromUri(Uri uri) {
    331     if (uri == null) {
    332       LogUtil.d("ContactInfoHelper.lookupContactFromUri", "uri is null");
    333       return null;
    334     }
    335     if (!PermissionsUtil.hasContactsReadPermissions(mContext)) {
    336       LogUtil.d("ContactInfoHelper.lookupContactFromUri", "no contact permission, return empty");
    337       return ContactInfo.EMPTY;
    338     }
    339 
    340     Cursor phoneLookupCursor = null;
    341     try {
    342       String[] projection = PhoneQuery.getPhoneLookupProjection(uri);
    343       phoneLookupCursor = mContext.getContentResolver().query(uri, projection, null, null, null);
    344     } catch (NullPointerException e) {
    345       LogUtil.e("ContactInfoHelper.lookupContactFromUri", "phone lookup", e);
    346       // Trap NPE from pre-N CP2
    347       return null;
    348     }
    349     if (phoneLookupCursor == null) {
    350       LogUtil.d("ContactInfoHelper.lookupContactFromUri", "phoneLookupCursor is null");
    351       return null;
    352     }
    353 
    354     try {
    355       if (!phoneLookupCursor.moveToFirst()) {
    356         return ContactInfo.EMPTY;
    357       }
    358       String lookupKey = phoneLookupCursor.getString(PhoneQuery.LOOKUP_KEY);
    359       ContactInfo contactInfo = createPhoneLookupContactInfo(phoneLookupCursor, lookupKey);
    360       fillAdditionalContactInfo(mContext, contactInfo);
    361       return contactInfo;
    362     } finally {
    363       phoneLookupCursor.close();
    364     }
    365   }
    366 
    367   private ContactInfo createPhoneLookupContactInfo(Cursor phoneLookupCursor, String lookupKey) {
    368     ContactInfo info = new ContactInfo();
    369     info.lookupKey = lookupKey;
    370     info.lookupUri =
    371         Contacts.getLookupUri(phoneLookupCursor.getLong(PhoneQuery.PERSON_ID), lookupKey);
    372     info.name = phoneLookupCursor.getString(PhoneQuery.NAME);
    373     info.type = phoneLookupCursor.getInt(PhoneQuery.PHONE_TYPE);
    374     info.label = phoneLookupCursor.getString(PhoneQuery.LABEL);
    375     info.number = phoneLookupCursor.getString(PhoneQuery.MATCHED_NUMBER);
    376     info.normalizedNumber = phoneLookupCursor.getString(PhoneQuery.NORMALIZED_NUMBER);
    377     info.photoId = phoneLookupCursor.getLong(PhoneQuery.PHOTO_ID);
    378     info.photoUri = UriUtils.parseUriOrNull(phoneLookupCursor.getString(PhoneQuery.PHOTO_URI));
    379     info.formattedNumber = null;
    380     info.userType =
    381         ContactsUtils.determineUserType(null, phoneLookupCursor.getLong(PhoneQuery.PERSON_ID));
    382     info.contactExists = true;
    383 
    384     return info;
    385   }
    386 
    387   private void fillAdditionalContactInfo(Context context, ContactInfo contactInfo) {
    388     if (contactInfo.number == null) {
    389       return;
    390     }
    391     Uri uri = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(contactInfo.number));
    392     try (Cursor cursor =
    393         context
    394             .getContentResolver()
    395             .query(uri, PhoneQuery.ADDITIONAL_CONTACT_INFO_PROJECTION, null, null, null)) {
    396       if (cursor == null || !cursor.moveToFirst()) {
    397         return;
    398       }
    399       contactInfo.nameAlternative =
    400           cursor.getString(PhoneQuery.ADDITIONAL_CONTACT_INFO_DISPLAY_NAME_ALTERNATIVE);
    401       contactInfo.carrierPresence =
    402           cursor.getInt(PhoneQuery.ADDITIONAL_CONTACT_INFO_CARRIER_PRESENCE);
    403     }
    404   }
    405 
    406   /**
    407    * Determines the contact information for the given phone number.
    408    *
    409    * <p>It returns the contact info if found.
    410    *
    411    * <p>If no contact corresponds to the given phone number, returns {@link ContactInfo#EMPTY}.
    412    *
    413    * <p>If the lookup fails for some other reason, it returns null.
    414    */
    415   @SuppressWarnings("ReferenceEquality")
    416   private ContactInfo queryContactInfoForPhoneNumber(
    417       String number, String countryIso, long directoryId) {
    418     if (TextUtils.isEmpty(number)) {
    419       LogUtil.d("ContactInfoHelper.queryContactInfoForPhoneNumber", "number is empty");
    420       return null;
    421     }
    422 
    423     ContactInfo info = lookupContactFromUri(getContactInfoLookupUri(number, directoryId));
    424     if (info == null) {
    425       LogUtil.d("ContactInfoHelper.queryContactInfoForPhoneNumber", "info looked up is null");
    426     }
    427     if (info != null && info != ContactInfo.EMPTY) {
    428       info.formattedNumber = formatPhoneNumber(number, null, countryIso);
    429       if (directoryId == -1) {
    430         // Contact found in the default directory
    431         info.sourceType = ContactSource.Type.SOURCE_TYPE_DIRECTORY;
    432       } else {
    433         // Contact found in the extended directory specified by directoryId
    434         info.sourceType = ContactSource.Type.SOURCE_TYPE_EXTENDED;
    435       }
    436     } else if (mCachedNumberLookupService != null) {
    437       CachedContactInfo cacheInfo =
    438           mCachedNumberLookupService.lookupCachedContactFromNumber(mContext, number);
    439       if (cacheInfo != null) {
    440         if (!cacheInfo.getContactInfo().isBadData) {
    441           info = cacheInfo.getContactInfo();
    442         } else {
    443           LogUtil.i("ContactInfoHelper.queryContactInfoForPhoneNumber", "info is bad data");
    444         }
    445       }
    446     }
    447     return info;
    448   }
    449 
    450   /**
    451    * Format the given phone number
    452    *
    453    * @param number the number to be formatted.
    454    * @param normalizedNumber the normalized number of the given number.
    455    * @param countryIso the ISO 3166-1 two letters country code, the country's convention will be
    456    *     used to format the number if the normalized phone is null.
    457    * @return the formatted number, or the given number if it was formatted.
    458    */
    459   private String formatPhoneNumber(String number, String normalizedNumber, String countryIso) {
    460     if (TextUtils.isEmpty(number)) {
    461       return "";
    462     }
    463     // If "number" is really a SIP address, don't try to do any formatting at all.
    464     if (PhoneNumberHelper.isUriNumber(number)) {
    465       return number;
    466     }
    467     if (TextUtils.isEmpty(countryIso)) {
    468       countryIso = mCurrentCountryIso;
    469     }
    470     return PhoneNumberUtils.formatNumber(number, normalizedNumber, countryIso);
    471   }
    472 
    473   /**
    474    * Stores differences between the updated contact info and the current call log contact info.
    475    *
    476    * @param number The number of the contact.
    477    * @param countryIso The country associated with this number.
    478    * @param updatedInfo The updated contact info.
    479    * @param callLogInfo The call log entry's current contact info.
    480    */
    481   public void updateCallLogContactInfo(
    482       String number, String countryIso, ContactInfo updatedInfo, ContactInfo callLogInfo) {
    483     if (!PermissionsUtil.hasPermission(mContext, android.Manifest.permission.WRITE_CALL_LOG)) {
    484       return;
    485     }
    486 
    487     final ContentValues values = new ContentValues();
    488     boolean needsUpdate = false;
    489 
    490     if (callLogInfo != null) {
    491       if (!TextUtils.equals(updatedInfo.name, callLogInfo.name)) {
    492         values.put(Calls.CACHED_NAME, updatedInfo.name);
    493         needsUpdate = true;
    494       }
    495 
    496       if (updatedInfo.type != callLogInfo.type) {
    497         values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
    498         needsUpdate = true;
    499       }
    500 
    501       if (!TextUtils.equals(updatedInfo.label, callLogInfo.label)) {
    502         values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
    503         needsUpdate = true;
    504       }
    505 
    506       if (!UriUtils.areEqual(updatedInfo.lookupUri, callLogInfo.lookupUri)) {
    507         values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
    508         needsUpdate = true;
    509       }
    510 
    511       // Only replace the normalized number if the new updated normalized number isn't empty.
    512       if (!TextUtils.isEmpty(updatedInfo.normalizedNumber)
    513           && !TextUtils.equals(updatedInfo.normalizedNumber, callLogInfo.normalizedNumber)) {
    514         values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
    515         needsUpdate = true;
    516       }
    517 
    518       if (!TextUtils.equals(updatedInfo.number, callLogInfo.number)) {
    519         values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
    520         needsUpdate = true;
    521       }
    522 
    523       if (updatedInfo.photoId != callLogInfo.photoId) {
    524         values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
    525         needsUpdate = true;
    526       }
    527 
    528       final Uri updatedPhotoUriContactsOnly = UriUtils.nullForNonContactsUri(updatedInfo.photoUri);
    529       if (!UriUtils.areEqual(updatedPhotoUriContactsOnly, callLogInfo.photoUri)) {
    530         values.put(Calls.CACHED_PHOTO_URI, UriUtils.uriToString(updatedPhotoUriContactsOnly));
    531         needsUpdate = true;
    532       }
    533 
    534       if (!TextUtils.equals(updatedInfo.formattedNumber, callLogInfo.formattedNumber)) {
    535         values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
    536         needsUpdate = true;
    537       }
    538 
    539       if (!TextUtils.equals(updatedInfo.geoDescription, callLogInfo.geoDescription)) {
    540         values.put(Calls.GEOCODED_LOCATION, updatedInfo.geoDescription);
    541         needsUpdate = true;
    542       }
    543     } else {
    544       // No previous values, store all of them.
    545       values.put(Calls.CACHED_NAME, updatedInfo.name);
    546       values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
    547       values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
    548       values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
    549       values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
    550       values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
    551       values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
    552       values.put(
    553           Calls.CACHED_PHOTO_URI,
    554           UriUtils.uriToString(UriUtils.nullForNonContactsUri(updatedInfo.photoUri)));
    555       values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
    556       values.put(Calls.GEOCODED_LOCATION, updatedInfo.geoDescription);
    557       needsUpdate = true;
    558     }
    559 
    560     if (!needsUpdate) {
    561       return;
    562     }
    563 
    564     try {
    565       if (countryIso == null) {
    566         mContext
    567             .getContentResolver()
    568             .update(
    569                 TelecomUtil.getCallLogUri(mContext),
    570                 values,
    571                 Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " IS NULL",
    572                 new String[] {number});
    573       } else {
    574         mContext
    575             .getContentResolver()
    576             .update(
    577                 TelecomUtil.getCallLogUri(mContext),
    578                 values,
    579                 Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " = ?",
    580                 new String[] {number, countryIso});
    581       }
    582     } catch (SQLiteFullException e) {
    583       LogUtil.e(TAG, "Unable to update contact info in call log db", e);
    584     }
    585   }
    586 
    587   public void updateCachedNumberLookupService(ContactInfo updatedInfo) {
    588     if (mCachedNumberLookupService != null) {
    589       if (hasName(updatedInfo)) {
    590         CachedContactInfo cachedContactInfo =
    591             mCachedNumberLookupService.buildCachedContactInfo(updatedInfo);
    592         mCachedNumberLookupService.addContact(mContext, cachedContactInfo);
    593       }
    594     }
    595   }
    596 
    597   /**
    598    * Given a contact's sourceType, return true if the contact is a business
    599    *
    600    * @param sourceType sourceType of the contact. This is usually populated by {@link
    601    *     #mCachedNumberLookupService}.
    602    */
    603   public boolean isBusiness(ContactSource.Type sourceType) {
    604     return mCachedNumberLookupService != null && mCachedNumberLookupService.isBusiness(sourceType);
    605   }
    606 
    607   /**
    608    * This function looks at a contact's source and determines if the user can mark caller ids from
    609    * this source as invalid.
    610    *
    611    * @param sourceType The source type to be checked
    612    * @param objectId The ID of the Contact object.
    613    * @return true if contacts from this source can be marked with an invalid caller id
    614    */
    615   public boolean canReportAsInvalid(ContactSource.Type sourceType, String objectId) {
    616     return mCachedNumberLookupService != null
    617         && mCachedNumberLookupService.canReportAsInvalid(sourceType, objectId);
    618   }
    619 
    620   /**
    621    * Update ContactInfo by querying to Cequint Caller ID. Only name, geoDescription and photo uri
    622    * will be updated if available.
    623    */
    624   @WorkerThread
    625   public void updateFromCequintCallerId(
    626       @Nullable CequintCallerIdManager cequintCallerIdManager, ContactInfo info, String number) {
    627     Assert.isWorkerThread();
    628     if (!CequintCallerIdManager.isCequintCallerIdEnabled(mContext)) {
    629       return;
    630     }
    631     if (cequintCallerIdManager == null) {
    632       return;
    633     }
    634     CequintCallerIdContact cequintCallerIdContact =
    635         cequintCallerIdManager.getCequintCallerIdContact(mContext, number);
    636     if (cequintCallerIdContact == null) {
    637       return;
    638     }
    639     if (TextUtils.isEmpty(info.name) && !TextUtils.isEmpty(cequintCallerIdContact.name)) {
    640       info.name = cequintCallerIdContact.name;
    641     }
    642     if (!TextUtils.isEmpty(cequintCallerIdContact.geoDescription)) {
    643       info.geoDescription = cequintCallerIdContact.geoDescription;
    644       info.sourceType = ContactSource.Type.SOURCE_TYPE_CEQUINT_CALLER_ID;
    645     }
    646     // Only update photo if local lookup has no result.
    647     if (!info.contactExists && info.photoUri == null && cequintCallerIdContact.imageUrl != null) {
    648       info.photoUri = UriUtils.parseUriOrNull(cequintCallerIdContact.imageUrl);
    649     }
    650   }
    651 }
    652