Home | History | Annotate | Download | only in calllog
      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.calllog;
     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.DisplayNameSources;
     27 import android.provider.ContactsContract.PhoneLookup;
     28 import android.support.annotation.Nullable;
     29 import android.telephony.PhoneNumberUtils;
     30 import android.text.TextUtils;
     31 import android.util.Log;
     32 
     33 import com.android.contacts.common.ContactsUtils;
     34 import com.android.contacts.common.ContactsUtils.UserType;
     35 import com.android.contacts.common.compat.CompatUtils;
     36 import com.android.contacts.common.util.Constants;
     37 import com.android.contacts.common.util.PermissionsUtil;
     38 import com.android.contacts.common.util.PhoneNumberHelper;
     39 import com.android.contacts.common.util.UriUtils;
     40 import com.android.dialer.compat.DialerCompatUtils;
     41 import com.android.dialer.service.CachedNumberLookupService;
     42 import com.android.dialer.service.CachedNumberLookupService.CachedContactInfo;
     43 import com.android.dialer.util.TelecomUtil;
     44 import com.android.dialerbind.ObjectFactory;
     45 
     46 import org.json.JSONException;
     47 import org.json.JSONObject;
     48 
     49 /**
     50  * Utility class to look up the contact information for a given number.
     51  */
     52 public class ContactInfoHelper {
     53     private static final String TAG = ContactInfoHelper.class.getSimpleName();
     54 
     55     private final Context mContext;
     56     private final String mCurrentCountryIso;
     57 
     58     private static final CachedNumberLookupService mCachedNumberLookupService =
     59             ObjectFactory.newCachedNumberLookupService();
     60 
     61     public ContactInfoHelper(Context context, String currentCountryIso) {
     62         mContext = context;
     63         mCurrentCountryIso = currentCountryIso;
     64     }
     65 
     66     /**
     67      * Returns the contact information for the given number.
     68      * <p>
     69      * If the number does not match any contact, returns a contact info containing only the number
     70      * and the formatted number.
     71      * <p>
     72      * If an error occurs during the lookup, it returns null.
     73      *
     74      * @param number the number to look up
     75      * @param countryIso the country associated with this number
     76      */
     77     @Nullable
     78     public ContactInfo lookupNumber(String number, String countryIso) {
     79         if (TextUtils.isEmpty(number)) {
     80             return null;
     81         }
     82 
     83         ContactInfo info;
     84 
     85         if (PhoneNumberHelper.isUriNumber(number)) {
     86             // The number is a SIP address..
     87             info = lookupContactFromUri(getContactInfoLookupUri(number), true);
     88             if (info == null || info == ContactInfo.EMPTY) {
     89                 // If lookup failed, check if the "username" of the SIP address is a phone number.
     90                 String username = PhoneNumberHelper.getUsernameFromUriNumber(number);
     91                 if (PhoneNumberUtils.isGlobalPhoneNumber(username)) {
     92                     info = queryContactInfoForPhoneNumber(username, countryIso, true);
     93                 }
     94             }
     95         } else {
     96             // Look for a contact that has the given phone number.
     97             info = queryContactInfoForPhoneNumber(number, countryIso, false);
     98         }
     99 
    100         final ContactInfo updatedInfo;
    101         if (info == null) {
    102             // The lookup failed.
    103             updatedInfo = null;
    104         } else {
    105             // If we did not find a matching contact, generate an empty contact info for the number.
    106             if (info == ContactInfo.EMPTY) {
    107                 // Did not find a matching contact.
    108                 updatedInfo = new ContactInfo();
    109                 updatedInfo.number = number;
    110                 updatedInfo.formattedNumber = formatPhoneNumber(number, null, countryIso);
    111                 updatedInfo.normalizedNumber = PhoneNumberUtils.formatNumberToE164(
    112                         number, countryIso);
    113                 updatedInfo.lookupUri = createTemporaryContactUri(updatedInfo.formattedNumber);
    114             } else {
    115                 updatedInfo = info;
    116             }
    117         }
    118         return updatedInfo;
    119     }
    120 
    121     /**
    122      * Creates a JSON-encoded lookup uri for a unknown number without an associated contact
    123      *
    124      * @param number - Unknown phone number
    125      * @return JSON-encoded URI that can be used to perform a lookup when clicking on the quick
    126      *         contact card.
    127      */
    128     private static Uri createTemporaryContactUri(String number) {
    129         try {
    130             final JSONObject contactRows = new JSONObject().put(Phone.CONTENT_ITEM_TYPE,
    131                     new JSONObject().put(Phone.NUMBER, number).put(Phone.TYPE, Phone.TYPE_CUSTOM));
    132 
    133             final String jsonString = new JSONObject().put(Contacts.DISPLAY_NAME, number)
    134                     .put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.PHONE)
    135                     .put(Contacts.CONTENT_ITEM_TYPE, contactRows).toString();
    136 
    137             return Contacts.CONTENT_LOOKUP_URI
    138                     .buildUpon()
    139                     .appendPath(Constants.LOOKUP_URI_ENCODED)
    140                     .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
    141                             String.valueOf(Long.MAX_VALUE))
    142                     .encodedFragment(jsonString)
    143                     .build();
    144         } catch (JSONException e) {
    145             return null;
    146         }
    147     }
    148 
    149     /**
    150      * Looks up a contact using the given URI.
    151      * <p>
    152      * It returns null if an error occurs, {@link ContactInfo#EMPTY} if no matching contact is
    153      * found, or the {@link ContactInfo} for the given contact.
    154      * <p>
    155      * The {@link ContactInfo#formattedNumber} field is always set to {@code null} in the returned
    156      * value.
    157      */
    158     ContactInfo lookupContactFromUri(Uri uri, boolean isSip) {
    159         if (uri == null) {
    160             return null;
    161         }
    162         if (!PermissionsUtil.hasContactsPermissions(mContext)) {
    163             return ContactInfo.EMPTY;
    164         }
    165 
    166         Cursor phoneLookupCursor = null;
    167         try {
    168             String[] projection = PhoneQuery.getPhoneLookupProjection(uri);
    169             phoneLookupCursor = mContext.getContentResolver().query(uri, projection, null, null,
    170                     null);
    171         } catch (NullPointerException e) {
    172             // Trap NPE from pre-N CP2
    173             return null;
    174         }
    175         if (phoneLookupCursor == null) {
    176             return null;
    177         }
    178 
    179         try {
    180             if (!phoneLookupCursor.moveToFirst()) {
    181                 return ContactInfo.EMPTY;
    182             }
    183             String lookupKey = phoneLookupCursor.getString(PhoneQuery.LOOKUP_KEY);
    184             ContactInfo contactInfo = createPhoneLookupContactInfo(phoneLookupCursor, lookupKey);
    185             contactInfo.nameAlternative = lookUpDisplayNameAlternative(mContext, lookupKey,
    186                     contactInfo.userType);
    187             return contactInfo;
    188         } finally {
    189             phoneLookupCursor.close();
    190         }
    191     }
    192 
    193     private ContactInfo createPhoneLookupContactInfo(Cursor phoneLookupCursor, String lookupKey) {
    194         ContactInfo info = new ContactInfo();
    195         info.lookupKey = lookupKey;
    196         info.lookupUri = Contacts.getLookupUri(phoneLookupCursor.getLong(PhoneQuery.PERSON_ID),
    197                 lookupKey);
    198         info.name = phoneLookupCursor.getString(PhoneQuery.NAME);
    199         info.type = phoneLookupCursor.getInt(PhoneQuery.PHONE_TYPE);
    200         info.label = phoneLookupCursor.getString(PhoneQuery.LABEL);
    201         info.number = phoneLookupCursor.getString(PhoneQuery.MATCHED_NUMBER);
    202         info.normalizedNumber = phoneLookupCursor.getString(PhoneQuery.NORMALIZED_NUMBER);
    203         info.photoId = phoneLookupCursor.getLong(PhoneQuery.PHOTO_ID);
    204         info.photoUri = UriUtils.parseUriOrNull(phoneLookupCursor.getString(PhoneQuery.PHOTO_URI));
    205         info.formattedNumber = null;
    206         info.userType = ContactsUtils.determineUserType(null,
    207                 phoneLookupCursor.getLong(PhoneQuery.PERSON_ID));
    208 
    209         return info;
    210     }
    211 
    212     public static String lookUpDisplayNameAlternative(Context context, String lookupKey,
    213             @UserType long userType) {
    214         // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed.
    215         if (lookupKey == null || userType == ContactsUtils.USER_TYPE_WORK) {
    216             return null;
    217         }
    218         final Uri uri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey);
    219         Cursor cursor = null;
    220         try {
    221             cursor = context.getContentResolver().query(uri,
    222                     PhoneQuery.DISPLAY_NAME_ALTERNATIVE_PROJECTION, null, null, null);
    223 
    224             if (cursor != null && cursor.moveToFirst()) {
    225                 return cursor.getString(PhoneQuery.NAME_ALTERNATIVE);
    226             }
    227         } catch (IllegalArgumentException e) {
    228             // Avoid dialer crash when lookup key is not valid
    229         } finally {
    230             if (cursor != null) {
    231                 cursor.close();
    232             }
    233         }
    234 
    235         return null;
    236     }
    237 
    238     /**
    239      * Determines the contact information for the given phone number.
    240      * <p>
    241      * It returns the contact info if found.
    242      * <p>
    243      * If no contact corresponds to the given phone number, returns {@link ContactInfo#EMPTY}.
    244      * <p>
    245      * If the lookup fails for some other reason, it returns null.
    246      */
    247     private ContactInfo queryContactInfoForPhoneNumber(String number, String countryIso,
    248                                                        boolean isSip) {
    249         if (TextUtils.isEmpty(number)) {
    250             return null;
    251         }
    252 
    253         ContactInfo info = lookupContactFromUri(getContactInfoLookupUri(number), isSip);
    254         if (info != null && info != ContactInfo.EMPTY) {
    255             info.formattedNumber = formatPhoneNumber(number, null, countryIso);
    256         } else if (mCachedNumberLookupService != null) {
    257             CachedContactInfo cacheInfo =
    258                     mCachedNumberLookupService.lookupCachedContactFromNumber(mContext, number);
    259             if (cacheInfo != null) {
    260                 info = cacheInfo.getContactInfo().isBadData ? null : cacheInfo.getContactInfo();
    261             } else {
    262                 info = null;
    263             }
    264         }
    265         return info;
    266     }
    267 
    268     /**
    269      * Format the given phone number
    270      *
    271      * @param number the number to be formatted.
    272      * @param normalizedNumber the normalized number of the given number.
    273      * @param countryIso the ISO 3166-1 two letters country code, the country's convention will be
    274      *        used to format the number if the normalized phone is null.
    275      *
    276      * @return the formatted number, or the given number if it was formatted.
    277      */
    278     private String formatPhoneNumber(String number, String normalizedNumber, String countryIso) {
    279         if (TextUtils.isEmpty(number)) {
    280             return "";
    281         }
    282         // If "number" is really a SIP address, don't try to do any formatting at all.
    283         if (PhoneNumberHelper.isUriNumber(number)) {
    284             return number;
    285         }
    286         if (TextUtils.isEmpty(countryIso)) {
    287             countryIso = mCurrentCountryIso;
    288         }
    289         return PhoneNumberUtils.formatNumber(number, normalizedNumber, countryIso);
    290     }
    291 
    292     /**
    293      * Stores differences between the updated contact info and the current call log contact info.
    294      *
    295      * @param number The number of the contact.
    296      * @param countryIso The country associated with this number.
    297      * @param updatedInfo The updated contact info.
    298      * @param callLogInfo The call log entry's current contact info.
    299      */
    300     public void updateCallLogContactInfo(String number, String countryIso, ContactInfo updatedInfo,
    301             ContactInfo callLogInfo) {
    302         if (!PermissionsUtil.hasPermission(mContext, android.Manifest.permission.WRITE_CALL_LOG)) {
    303             return;
    304         }
    305 
    306         final ContentValues values = new ContentValues();
    307         boolean needsUpdate = false;
    308 
    309         if (callLogInfo != null) {
    310             if (!TextUtils.equals(updatedInfo.name, callLogInfo.name)) {
    311                 values.put(Calls.CACHED_NAME, updatedInfo.name);
    312                 needsUpdate = true;
    313             }
    314 
    315             if (updatedInfo.type != callLogInfo.type) {
    316                 values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
    317                 needsUpdate = true;
    318             }
    319 
    320             if (!TextUtils.equals(updatedInfo.label, callLogInfo.label)) {
    321                 values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
    322                 needsUpdate = true;
    323             }
    324 
    325             if (!UriUtils.areEqual(updatedInfo.lookupUri, callLogInfo.lookupUri)) {
    326                 values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
    327                 needsUpdate = true;
    328             }
    329 
    330             // Only replace the normalized number if the new updated normalized number isn't empty.
    331             if (!TextUtils.isEmpty(updatedInfo.normalizedNumber) &&
    332                     !TextUtils.equals(updatedInfo.normalizedNumber, callLogInfo.normalizedNumber)) {
    333                 values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
    334                 needsUpdate = true;
    335             }
    336 
    337             if (!TextUtils.equals(updatedInfo.number, callLogInfo.number)) {
    338                 values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
    339                 needsUpdate = true;
    340             }
    341 
    342             if (updatedInfo.photoId != callLogInfo.photoId) {
    343                 values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
    344                 needsUpdate = true;
    345             }
    346 
    347             final Uri updatedPhotoUriContactsOnly =
    348                     UriUtils.nullForNonContactsUri(updatedInfo.photoUri);
    349             if (DialerCompatUtils.isCallsCachedPhotoUriCompatible() &&
    350                     !UriUtils.areEqual(updatedPhotoUriContactsOnly, callLogInfo.photoUri)) {
    351                 values.put(Calls.CACHED_PHOTO_URI,
    352                         UriUtils.uriToString(updatedPhotoUriContactsOnly));
    353                 needsUpdate = true;
    354             }
    355 
    356             if (!TextUtils.equals(updatedInfo.formattedNumber, callLogInfo.formattedNumber)) {
    357                 values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
    358                 needsUpdate = true;
    359             }
    360         } else {
    361             // No previous values, store all of them.
    362             values.put(Calls.CACHED_NAME, updatedInfo.name);
    363             values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
    364             values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
    365             values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
    366             values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
    367             values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
    368             values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
    369             if (DialerCompatUtils.isCallsCachedPhotoUriCompatible()) {
    370                 values.put(Calls.CACHED_PHOTO_URI, UriUtils.uriToString(
    371                         UriUtils.nullForNonContactsUri(updatedInfo.photoUri)));
    372             }
    373             values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
    374             needsUpdate = true;
    375         }
    376 
    377         if (!needsUpdate) {
    378             return;
    379         }
    380 
    381         try {
    382             if (countryIso == null) {
    383                 mContext.getContentResolver().update(
    384                         TelecomUtil.getCallLogUri(mContext),
    385                         values,
    386                         Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " IS NULL",
    387                         new String[]{ number });
    388             } else {
    389                 mContext.getContentResolver().update(
    390                         TelecomUtil.getCallLogUri(mContext),
    391                         values,
    392                         Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " = ?",
    393                         new String[]{ number, countryIso });
    394             }
    395         } catch (SQLiteFullException e) {
    396             Log.e(TAG, "Unable to update contact info in call log db", e);
    397         }
    398     }
    399 
    400     public static Uri getContactInfoLookupUri(String number) {
    401         return getContactInfoLookupUri(number, -1);
    402     }
    403 
    404     public static Uri getContactInfoLookupUri(String number, long directoryId) {
    405         // Get URI for the number in the PhoneLookup table, with a parameter to indicate whether
    406         // the number is a SIP number.
    407         Uri uri = PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI;
    408         if (!ContactsUtils.FLAG_N_FEATURE) {
    409             if (directoryId != -1) {
    410                 // ENTERPRISE_CONTENT_FILTER_URI in M doesn't support directory lookup
    411                 uri = PhoneLookup.CONTENT_FILTER_URI;
    412             } else {
    413                 // b/25900607 in M. PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, encodes twice.
    414                 number = Uri.encode(number);
    415             }
    416         }
    417         Uri.Builder builder = uri.buildUpon()
    418                 .appendPath(number)
    419                 .appendQueryParameter(PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS,
    420                 String.valueOf(PhoneNumberHelper.isUriNumber(number)));
    421         if (directoryId != -1) {
    422             builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
    423                     String.valueOf(directoryId));
    424         }
    425         return builder.build();
    426     }
    427 
    428     /**
    429      * Returns the contact information stored in an entry of the call log.
    430      *
    431      * @param c A cursor pointing to an entry in the call log.
    432      */
    433     public static ContactInfo getContactInfo(Cursor c) {
    434         ContactInfo info = new ContactInfo();
    435         info.lookupUri = UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_LOOKUP_URI));
    436         info.name = c.getString(CallLogQuery.CACHED_NAME);
    437         info.type = c.getInt(CallLogQuery.CACHED_NUMBER_TYPE);
    438         info.label = c.getString(CallLogQuery.CACHED_NUMBER_LABEL);
    439         String matchedNumber = c.getString(CallLogQuery.CACHED_MATCHED_NUMBER);
    440         String postDialDigits = CompatUtils.isNCompatible()
    441                 ? c.getString(CallLogQuery.POST_DIAL_DIGITS) : "";
    442         info.number = (matchedNumber == null) ?
    443                 c.getString(CallLogQuery.NUMBER) + postDialDigits : matchedNumber;
    444 
    445         info.normalizedNumber = c.getString(CallLogQuery.CACHED_NORMALIZED_NUMBER);
    446         info.photoId = c.getLong(CallLogQuery.CACHED_PHOTO_ID);
    447         info.photoUri = DialerCompatUtils.isCallsCachedPhotoUriCompatible() ?
    448                 UriUtils.nullForNonContactsUri(
    449                         UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_PHOTO_URI)))
    450                 : null;
    451         info.formattedNumber = c.getString(CallLogQuery.CACHED_FORMATTED_NUMBER);
    452 
    453         return info;
    454     }
    455 
    456     /**
    457      * Given a contact's sourceType, return true if the contact is a business
    458      *
    459      * @param sourceType sourceType of the contact. This is usually populated by
    460      *        {@link #mCachedNumberLookupService}.
    461      */
    462     public boolean isBusiness(int sourceType) {
    463         return mCachedNumberLookupService != null
    464                 && mCachedNumberLookupService.isBusiness(sourceType);
    465     }
    466 
    467     /**
    468      * This function looks at a contact's source and determines if the user can
    469      * mark caller ids from this source as invalid.
    470      *
    471      * @param sourceType The source type to be checked
    472      * @param objectId The ID of the Contact object.
    473      * @return true if contacts from this source can be marked with an invalid caller id
    474      */
    475     public boolean canReportAsInvalid(int sourceType, String objectId) {
    476         return mCachedNumberLookupService != null
    477                 && mCachedNumberLookupService.canReportAsInvalid(sourceType, objectId);
    478     }
    479 }
    480