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