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.calllog; 16 17import android.content.ContentValues; 18import android.content.Context; 19import android.database.Cursor; 20import android.database.sqlite.SQLiteFullException; 21import android.net.Uri; 22import android.provider.CallLog.Calls; 23import android.provider.ContactsContract; 24import android.provider.ContactsContract.CommonDataKinds.Phone; 25import android.provider.ContactsContract.Contacts; 26import android.provider.ContactsContract.DisplayNameSources; 27import android.provider.ContactsContract.PhoneLookup; 28import android.telephony.PhoneNumberUtils; 29import android.text.TextUtils; 30import android.util.Log; 31 32import com.android.contacts.common.util.Constants; 33import com.android.contacts.common.util.PermissionsUtil; 34import com.android.contacts.common.util.PhoneNumberHelper; 35import com.android.contacts.common.util.UriUtils; 36import com.android.dialer.service.CachedNumberLookupService; 37import com.android.dialer.service.CachedNumberLookupService.CachedContactInfo; 38import com.android.dialer.util.TelecomUtil; 39import com.android.dialerbind.ObjectFactory; 40 41import org.json.JSONException; 42import org.json.JSONObject; 43 44import java.util.List; 45 46/** 47 * Utility class to look up the contact information for a given number. 48 */ 49public 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