ContactInfoHelper.java revision e7ea93d5235c097151e40f8922a1efb000734904
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
15package com.android.dialer.phonenumbercache;
16
17import android.annotation.TargetApi;
18import android.content.ContentResolver;
19import android.content.ContentValues;
20import android.content.Context;
21import android.database.Cursor;
22import android.database.sqlite.SQLiteFullException;
23import android.net.Uri;
24import android.os.Build.VERSION;
25import android.os.Build.VERSION_CODES;
26import android.provider.CallLog.Calls;
27import android.provider.ContactsContract;
28import android.provider.ContactsContract.CommonDataKinds.Phone;
29import android.provider.ContactsContract.Contacts;
30import android.provider.ContactsContract.Directory;
31import android.provider.ContactsContract.DisplayNameSources;
32import android.provider.ContactsContract.PhoneLookup;
33import android.support.annotation.Nullable;
34import android.support.annotation.WorkerThread;
35import android.telephony.PhoneNumberUtils;
36import android.text.TextUtils;
37import com.android.contacts.common.ContactsUtils;
38import com.android.contacts.common.ContactsUtils.UserType;
39import com.android.contacts.common.compat.DirectoryCompat;
40import com.android.contacts.common.util.Constants;
41import com.android.dialer.common.Assert;
42import com.android.dialer.common.LogUtil;
43import com.android.dialer.logging.ContactSource;
44import com.android.dialer.oem.CequintCallerIdManager;
45import com.android.dialer.oem.CequintCallerIdManager.CequintCallerIdContact;
46import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo;
47import com.android.dialer.phonenumberutil.PhoneNumberHelper;
48import com.android.dialer.telecom.TelecomUtil;
49import com.android.dialer.util.PermissionsUtil;
50import com.android.dialer.util.UriUtils;
51import java.util.ArrayList;
52import java.util.List;
53import org.json.JSONException;
54import 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)
59public 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        // a bug 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    try (Cursor phoneLookupCursor =
341        mContext
342            .getContentResolver()
343            .query(
344                uri,
345                PhoneQuery.getPhoneLookupProjection(uri),
346                null /* selection */,
347                null /* selectionArgs */,
348                null /* sortOrder */)) {
349      if (phoneLookupCursor == null) {
350        LogUtil.d("ContactInfoHelper.lookupContactFromUri", "phoneLookupCursor is null");
351        return null;
352      }
353
354      if (!phoneLookupCursor.moveToFirst()) {
355        return ContactInfo.EMPTY;
356      }
357
358      String lookupKey = phoneLookupCursor.getString(PhoneQuery.LOOKUP_KEY);
359      ContactInfo contactInfo = createPhoneLookupContactInfo(phoneLookupCursor, lookupKey);
360      fillAdditionalContactInfo(mContext, contactInfo);
361      return contactInfo;
362    }
363  }
364
365  private ContactInfo createPhoneLookupContactInfo(Cursor phoneLookupCursor, String lookupKey) {
366    ContactInfo info = new ContactInfo();
367    info.lookupKey = lookupKey;
368    info.lookupUri =
369        Contacts.getLookupUri(phoneLookupCursor.getLong(PhoneQuery.PERSON_ID), lookupKey);
370    info.name = phoneLookupCursor.getString(PhoneQuery.NAME);
371    info.type = phoneLookupCursor.getInt(PhoneQuery.PHONE_TYPE);
372    info.label = phoneLookupCursor.getString(PhoneQuery.LABEL);
373    info.number = phoneLookupCursor.getString(PhoneQuery.MATCHED_NUMBER);
374    info.normalizedNumber = phoneLookupCursor.getString(PhoneQuery.NORMALIZED_NUMBER);
375    info.photoId = phoneLookupCursor.getLong(PhoneQuery.PHOTO_ID);
376    info.photoUri = UriUtils.parseUriOrNull(phoneLookupCursor.getString(PhoneQuery.PHOTO_URI));
377    info.formattedNumber = null;
378    info.userType =
379        ContactsUtils.determineUserType(null, phoneLookupCursor.getLong(PhoneQuery.PERSON_ID));
380    info.contactExists = true;
381
382    return info;
383  }
384
385  private void fillAdditionalContactInfo(Context context, ContactInfo contactInfo) {
386    if (contactInfo.number == null) {
387      return;
388    }
389    Uri uri = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(contactInfo.number));
390    try (Cursor cursor =
391        context
392            .getContentResolver()
393            .query(uri, PhoneQuery.ADDITIONAL_CONTACT_INFO_PROJECTION, null, null, null)) {
394      if (cursor == null || !cursor.moveToFirst()) {
395        return;
396      }
397      contactInfo.nameAlternative =
398          cursor.getString(PhoneQuery.ADDITIONAL_CONTACT_INFO_DISPLAY_NAME_ALTERNATIVE);
399      contactInfo.carrierPresence =
400          cursor.getInt(PhoneQuery.ADDITIONAL_CONTACT_INFO_CARRIER_PRESENCE);
401    }
402  }
403
404  /**
405   * Determines the contact information for the given phone number.
406   *
407   * <p>It returns the contact info if found.
408   *
409   * <p>If no contact corresponds to the given phone number, returns {@link ContactInfo#EMPTY}.
410   *
411   * <p>If the lookup fails for some other reason, it returns null.
412   */
413  @SuppressWarnings("ReferenceEquality")
414  private ContactInfo queryContactInfoForPhoneNumber(
415      String number, String countryIso, long directoryId) {
416    if (TextUtils.isEmpty(number)) {
417      LogUtil.d("ContactInfoHelper.queryContactInfoForPhoneNumber", "number is empty");
418      return null;
419    }
420
421    ContactInfo info = lookupContactFromUri(getContactInfoLookupUri(number, directoryId));
422    if (info == null) {
423      LogUtil.d("ContactInfoHelper.queryContactInfoForPhoneNumber", "info looked up is null");
424    }
425    if (info != null && info != ContactInfo.EMPTY) {
426      info.formattedNumber = formatPhoneNumber(number, null, countryIso);
427      if (directoryId == -1) {
428        // Contact found in the default directory
429        info.sourceType = ContactSource.Type.SOURCE_TYPE_DIRECTORY;
430      } else {
431        // Contact found in the extended directory specified by directoryId
432        info.sourceType = ContactSource.Type.SOURCE_TYPE_EXTENDED;
433      }
434    } else if (mCachedNumberLookupService != null) {
435      CachedContactInfo cacheInfo =
436          mCachedNumberLookupService.lookupCachedContactFromNumber(mContext, number);
437      if (cacheInfo != null) {
438        if (!cacheInfo.getContactInfo().isBadData) {
439          info = cacheInfo.getContactInfo();
440        } else {
441          LogUtil.i("ContactInfoHelper.queryContactInfoForPhoneNumber", "info is bad data");
442        }
443      }
444    }
445    return info;
446  }
447
448  /**
449   * Format the given phone number
450   *
451   * @param number the number to be formatted.
452   * @param normalizedNumber the normalized number of the given number.
453   * @param countryIso the ISO 3166-1 two letters country code, the country's convention will be
454   *     used to format the number if the normalized phone is null.
455   * @return the formatted number, or the given number if it was formatted.
456   */
457  private String formatPhoneNumber(String number, String normalizedNumber, String countryIso) {
458    if (TextUtils.isEmpty(number)) {
459      return "";
460    }
461    // If "number" is really a SIP address, don't try to do any formatting at all.
462    if (PhoneNumberHelper.isUriNumber(number)) {
463      return number;
464    }
465    if (TextUtils.isEmpty(countryIso)) {
466      countryIso = mCurrentCountryIso;
467    }
468    return PhoneNumberUtils.formatNumber(number, normalizedNumber, countryIso);
469  }
470
471  /**
472   * Stores differences between the updated contact info and the current call log contact info.
473   *
474   * @param number The number of the contact.
475   * @param countryIso The country associated with this number.
476   * @param updatedInfo The updated contact info.
477   * @param callLogInfo The call log entry's current contact info.
478   */
479  public void updateCallLogContactInfo(
480      String number, String countryIso, ContactInfo updatedInfo, ContactInfo callLogInfo) {
481    if (!PermissionsUtil.hasPermission(mContext, android.Manifest.permission.WRITE_CALL_LOG)) {
482      return;
483    }
484
485    final ContentValues values = new ContentValues();
486    boolean needsUpdate = false;
487
488    if (callLogInfo != null) {
489      if (!TextUtils.equals(updatedInfo.name, callLogInfo.name)) {
490        values.put(Calls.CACHED_NAME, updatedInfo.name);
491        needsUpdate = true;
492      }
493
494      if (updatedInfo.type != callLogInfo.type) {
495        values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
496        needsUpdate = true;
497      }
498
499      if (!TextUtils.equals(updatedInfo.label, callLogInfo.label)) {
500        values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
501        needsUpdate = true;
502      }
503
504      if (!UriUtils.areEqual(updatedInfo.lookupUri, callLogInfo.lookupUri)) {
505        values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
506        needsUpdate = true;
507      }
508
509      // Only replace the normalized number if the new updated normalized number isn't empty.
510      if (!TextUtils.isEmpty(updatedInfo.normalizedNumber)
511          && !TextUtils.equals(updatedInfo.normalizedNumber, callLogInfo.normalizedNumber)) {
512        values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
513        needsUpdate = true;
514      }
515
516      if (!TextUtils.equals(updatedInfo.number, callLogInfo.number)) {
517        values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
518        needsUpdate = true;
519      }
520
521      if (updatedInfo.photoId != callLogInfo.photoId) {
522        values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
523        needsUpdate = true;
524      }
525
526      final Uri updatedPhotoUriContactsOnly = UriUtils.nullForNonContactsUri(updatedInfo.photoUri);
527      if (!UriUtils.areEqual(updatedPhotoUriContactsOnly, callLogInfo.photoUri)) {
528        values.put(Calls.CACHED_PHOTO_URI, UriUtils.uriToString(updatedPhotoUriContactsOnly));
529        needsUpdate = true;
530      }
531
532      if (!TextUtils.equals(updatedInfo.formattedNumber, callLogInfo.formattedNumber)) {
533        values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
534        needsUpdate = true;
535      }
536
537      if (!TextUtils.equals(updatedInfo.geoDescription, callLogInfo.geoDescription)) {
538        values.put(Calls.GEOCODED_LOCATION, updatedInfo.geoDescription);
539        needsUpdate = true;
540      }
541    } else {
542      // No previous values, store all of them.
543      values.put(Calls.CACHED_NAME, updatedInfo.name);
544      values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
545      values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
546      values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
547      values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
548      values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
549      values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
550      values.put(
551          Calls.CACHED_PHOTO_URI,
552          UriUtils.uriToString(UriUtils.nullForNonContactsUri(updatedInfo.photoUri)));
553      values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
554      values.put(Calls.GEOCODED_LOCATION, updatedInfo.geoDescription);
555      needsUpdate = true;
556    }
557
558    if (!needsUpdate) {
559      return;
560    }
561
562    try {
563      if (countryIso == null) {
564        mContext
565            .getContentResolver()
566            .update(
567                TelecomUtil.getCallLogUri(mContext),
568                values,
569                Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " IS NULL",
570                new String[] {number});
571      } else {
572        mContext
573            .getContentResolver()
574            .update(
575                TelecomUtil.getCallLogUri(mContext),
576                values,
577                Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " = ?",
578                new String[] {number, countryIso});
579      }
580    } catch (SQLiteFullException e) {
581      LogUtil.e(TAG, "Unable to update contact info in call log db", e);
582    }
583  }
584
585  public void updateCachedNumberLookupService(ContactInfo updatedInfo) {
586    if (mCachedNumberLookupService != null) {
587      if (hasName(updatedInfo)) {
588        CachedContactInfo cachedContactInfo =
589            mCachedNumberLookupService.buildCachedContactInfo(updatedInfo);
590        mCachedNumberLookupService.addContact(mContext, cachedContactInfo);
591      }
592    }
593  }
594
595  /**
596   * Given a contact's sourceType, return true if the contact is a business
597   *
598   * @param sourceType sourceType of the contact. This is usually populated by {@link
599   *     #mCachedNumberLookupService}.
600   */
601  public boolean isBusiness(ContactSource.Type sourceType) {
602    return mCachedNumberLookupService != null && mCachedNumberLookupService.isBusiness(sourceType);
603  }
604
605  /**
606   * This function looks at a contact's source and determines if the user can mark caller ids from
607   * this source as invalid.
608   *
609   * @param sourceType The source type to be checked
610   * @param objectId The ID of the Contact object.
611   * @return true if contacts from this source can be marked with an invalid caller id
612   */
613  public boolean canReportAsInvalid(ContactSource.Type sourceType, String objectId) {
614    return mCachedNumberLookupService != null
615        && mCachedNumberLookupService.canReportAsInvalid(sourceType, objectId);
616  }
617
618  /**
619   * Update ContactInfo by querying to Cequint Caller ID. Only name, geoDescription and photo uri
620   * will be updated if available.
621   */
622  @WorkerThread
623  public void updateFromCequintCallerId(
624      @Nullable CequintCallerIdManager cequintCallerIdManager, ContactInfo info, String number) {
625    Assert.isWorkerThread();
626    if (!CequintCallerIdManager.isCequintCallerIdEnabled(mContext)) {
627      return;
628    }
629    if (cequintCallerIdManager == null) {
630      return;
631    }
632    CequintCallerIdContact cequintCallerIdContact =
633        cequintCallerIdManager.getCequintCallerIdContact(mContext, number);
634    if (cequintCallerIdContact == null) {
635      return;
636    }
637    if (TextUtils.isEmpty(info.name) && !TextUtils.isEmpty(cequintCallerIdContact.name)) {
638      info.name = cequintCallerIdContact.name;
639    }
640    if (!TextUtils.isEmpty(cequintCallerIdContact.geoDescription)) {
641      info.geoDescription = cequintCallerIdContact.geoDescription;
642      info.sourceType = ContactSource.Type.SOURCE_TYPE_CEQUINT_CALLER_ID;
643    }
644    // Only update photo if local lookup has no result.
645    if (!info.contactExists && info.photoUri == null && cequintCallerIdContact.imageUrl != null) {
646      info.photoUri = UriUtils.parseUriOrNull(cequintCallerIdContact.imageUrl);
647    }
648  }
649}
650