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