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