• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
ContactInfoHelper(Context context, String currentCountryIso)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    */
createTemporaryContactUri(String number)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 
lookUpDisplayNameAlternative( Context context, String lookupKey, @UserType long userType, @Nullable Long directoryId)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 
getContactInfoLookupUri(String number)148   public static Uri getContactInfoLookupUri(String number) {
149     return getContactInfoLookupUri(number, -1);
150   }
151 
getContactInfoLookupUri(String number, long directoryId)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    */
getContactInfo(Cursor c)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
lookupNumber(String number, String countryIso)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")
lookupNumber(String number, String countryIso, long directoryId)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 
createEmptyContactInfoForNumber(String number, String countryIso)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    */
lookupNumberInRemoteDirectory(String number, String countryIso)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 
hasName(ContactInfo contactInfo)291   public boolean hasName(ContactInfo contactInfo) {
292     return contactInfo != null && !TextUtils.isEmpty(contactInfo.name);
293   }
294 
getRemoteDirectories(Context context)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    */
lookupContactFromUri(Uri uri)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 
createPhoneLookupContactInfo(Cursor phoneLookupCursor, String lookupKey)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 
fillAdditionalContactInfo(Context context, ContactInfo contactInfo)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")
queryContactInfoForPhoneNumber( String number, String countryIso, long directoryId)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    */
formatPhoneNumber(String number, String normalizedNumber, String countryIso)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    */
updateCallLogContactInfo( String number, String countryIso, ContactInfo updatedInfo, ContactInfo callLogInfo)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 
updateCachedNumberLookupService(ContactInfo updatedInfo)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    */
isBusiness(ContactSource.Type sourceType)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    */
canReportAsInvalid(ContactSource.Type sourceType, String objectId)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
updateFromCequintCallerId( @ullable CequintCallerIdManager cequintCallerIdManager, ContactInfo info, String number)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