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