1 /* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.incallui; 18 19 import android.content.Context; 20 import android.database.Cursor; 21 import android.graphics.Bitmap; 22 import android.graphics.drawable.Drawable; 23 import android.net.Uri; 24 import android.provider.ContactsContract; 25 import android.provider.ContactsContract.CommonDataKinds.Phone; 26 import android.provider.ContactsContract.Contacts; 27 import android.provider.ContactsContract.Data; 28 import android.provider.ContactsContract.PhoneLookup; 29 import android.provider.ContactsContract.RawContacts; 30 import android.telephony.PhoneNumberUtils; 31 import android.text.TextUtils; 32 import com.android.contacts.common.ContactsUtils; 33 import com.android.contacts.common.ContactsUtils.UserType; 34 import com.android.contacts.common.util.TelephonyManagerUtils; 35 import com.android.dialer.logging.ContactLookupResult; 36 import com.android.dialer.phonenumbercache.ContactInfoHelper; 37 import com.android.dialer.phonenumberutil.PhoneNumberHelper; 38 39 /** 40 * Looks up caller information for the given phone number. This is intermediate data and should NOT 41 * be used by any UI. 42 */ 43 public class CallerInfo { 44 45 private static final String TAG = "CallerInfo"; 46 47 private static final String[] DEFAULT_PHONELOOKUP_PROJECTION = 48 new String[] { 49 PhoneLookup.CONTACT_ID, 50 PhoneLookup.DISPLAY_NAME, 51 PhoneLookup.LOOKUP_KEY, 52 PhoneLookup.NUMBER, 53 PhoneLookup.NORMALIZED_NUMBER, 54 PhoneLookup.LABEL, 55 PhoneLookup.TYPE, 56 PhoneLookup.PHOTO_URI, 57 PhoneLookup.CUSTOM_RINGTONE, 58 PhoneLookup.SEND_TO_VOICEMAIL 59 }; 60 61 /** 62 * Please note that, any one of these member variables can be null, and any accesses to them 63 * should be prepared to handle such a case. 64 * 65 * <p>Also, it is implied that phoneNumber is more often populated than name is, (think of calls 66 * being dialed/received using numbers where names are not known to the device), so phoneNumber 67 * should serve as a dependable fallback when name is unavailable. 68 * 69 * <p>One other detail here is that this CallerInfo object reflects information found on a 70 * connection, it is an OUTPUT that serves mainly to display information to the user. In no way is 71 * this object used as input to make a connection, so we can choose to display whatever 72 * human-readable text makes sense to the user for a connection. This is especially relevant for 73 * the phone number field, since it is the one field that is most likely exposed to the user. 74 * 75 * <p>As an example: 1. User dials "911" 2. Device recognizes that this is an emergency number 3. 76 * We use the "Emergency Number" string instead of "911" in the phoneNumber field. 77 * 78 * <p>What we're really doing here is treating phoneNumber as an essential field here, NOT name. 79 * We're NOT always guaranteed to have a name for a connection, but the number should be 80 * displayable. 81 */ 82 public String name; 83 84 public String nameAlternative; 85 public String phoneNumber; 86 public String normalizedNumber; 87 public String forwardingNumber; 88 public String geoDescription; 89 boolean shouldShowGeoDescription; 90 public String cnapName; 91 public int numberPresentation; 92 public int namePresentation; 93 public boolean contactExists; 94 public ContactLookupResult.Type contactLookupResultType = ContactLookupResult.Type.NOT_FOUND; 95 public String phoneLabel; 96 /* Split up the phoneLabel into number type and label name */ 97 public int numberType; 98 public String numberLabel; 99 public int photoResource; 100 // Contact ID, which will be 0 if a contact comes from the corp CP2. 101 public long contactIdOrZero; 102 public String lookupKeyOrNull; 103 public boolean needUpdate; 104 public Uri contactRefUri; 105 public @UserType long userType; 106 /** 107 * Contact display photo URI. If a contact has no display photo but a thumbnail, it'll be the 108 * thumbnail URI instead. 109 */ 110 public Uri contactDisplayPhotoUri; 111 // fields to hold individual contact preference data, 112 // including the send to voicemail flag and the ringtone 113 // uri reference. 114 public Uri contactRingtoneUri; 115 public boolean shouldSendToVoicemail; 116 /** 117 * Drawable representing the caller image. This is essentially a cache for the image data tied 118 * into the connection / callerinfo object. 119 * 120 * <p>This might be a high resolution picture which is more suitable for full-screen image view 121 * than for smaller icons used in some kinds of notifications. 122 * 123 * <p>The {@link #isCachedPhotoCurrent} flag indicates if the image data needs to be reloaded. 124 */ 125 public Drawable cachedPhoto; 126 /** 127 * Bitmap representing the caller image which has possibly lower resolution than {@link 128 * #cachedPhoto} and thus more suitable for icons (like notification icons). 129 * 130 * <p>In usual cases this is just down-scaled image of {@link #cachedPhoto}. If the down-scaling 131 * fails, this will just become null. 132 * 133 * <p>The {@link #isCachedPhotoCurrent} flag indicates if the image data needs to be reloaded. 134 */ 135 public Bitmap cachedPhotoIcon; 136 /** 137 * Boolean which indicates if {@link #cachedPhoto} and {@link #cachedPhotoIcon} is fresh enough. 138 * If it is false, those images aren't pointing to valid objects. 139 */ 140 public boolean isCachedPhotoCurrent; 141 /** 142 * String which holds the call subject sent as extra from the lower layers for this call. This is 143 * used to display the no-caller ID reason for restricted/unknown number presentation. 144 */ 145 public String callSubject; 146 147 public String countryIso; 148 149 private boolean isEmergency; 150 private boolean isVoiceMail; 151 CallerInfo()152 public CallerInfo() { 153 // TODO: Move all the basic initialization here? 154 isEmergency = false; 155 isVoiceMail = false; 156 userType = ContactsUtils.USER_TYPE_CURRENT; 157 } 158 getDefaultPhoneLookupProjection()159 static String[] getDefaultPhoneLookupProjection() { 160 return DEFAULT_PHONELOOKUP_PROJECTION; 161 } 162 163 /** 164 * getCallerInfo given a Cursor. 165 * 166 * @param context the context used to retrieve string constants 167 * @param contactRef the URI to attach to this CallerInfo object 168 * @param cursor the first object in the cursor is used to build the CallerInfo object. 169 * @return the CallerInfo which contains the caller id for the given number. The returned 170 * CallerInfo is null if no number is supplied. 171 */ getCallerInfo(Context context, Uri contactRef, Cursor cursor)172 public static CallerInfo getCallerInfo(Context context, Uri contactRef, Cursor cursor) { 173 CallerInfo info = new CallerInfo(); 174 info.cachedPhoto = null; 175 info.contactExists = false; 176 info.contactRefUri = contactRef; 177 info.isCachedPhotoCurrent = false; 178 info.name = null; 179 info.needUpdate = false; 180 info.numberLabel = null; 181 info.numberType = 0; 182 info.phoneLabel = null; 183 info.photoResource = 0; 184 info.userType = ContactsUtils.USER_TYPE_CURRENT; 185 186 Log.v(TAG, "getCallerInfo() based on cursor..."); 187 188 if (cursor == null || !cursor.moveToFirst()) { 189 return info; 190 } 191 192 // TODO: photo_id is always available but not taken 193 // care of here. Maybe we should store it in the 194 // CallerInfo object as well. 195 196 long contactId = 0L; 197 int columnIndex; 198 199 // Look for the number 200 columnIndex = cursor.getColumnIndex(PhoneLookup.NUMBER); 201 if (columnIndex != -1) { 202 // The Contacts provider ignores special characters in phone numbers when searching for a 203 // contact. For example, number "123" is considered a match with a contact with number "#123". 204 // We need to check whether the result contains a number that truly matches the query and move 205 // the cursor to that position before filling in the fields in CallerInfo. 206 boolean hasNumberMatch = 207 PhoneNumberHelper.updateCursorToMatchContactLookupUri(cursor, columnIndex, contactRef); 208 if (hasNumberMatch) { 209 info.phoneNumber = cursor.getString(columnIndex); 210 } else { 211 return info; 212 } 213 } 214 215 // Look for the name 216 columnIndex = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME); 217 if (columnIndex != -1) { 218 info.name = normalize(cursor.getString(columnIndex)); 219 } 220 221 // Look for the normalized number 222 columnIndex = cursor.getColumnIndex(PhoneLookup.NORMALIZED_NUMBER); 223 if (columnIndex != -1) { 224 info.normalizedNumber = cursor.getString(columnIndex); 225 } 226 227 // Look for the label/type combo 228 columnIndex = cursor.getColumnIndex(PhoneLookup.LABEL); 229 if (columnIndex != -1) { 230 int typeColumnIndex = cursor.getColumnIndex(PhoneLookup.TYPE); 231 if (typeColumnIndex != -1) { 232 info.numberType = cursor.getInt(typeColumnIndex); 233 info.numberLabel = cursor.getString(columnIndex); 234 info.phoneLabel = 235 Phone.getTypeLabel(context.getResources(), info.numberType, info.numberLabel) 236 .toString(); 237 } 238 } 239 240 // cache the lookup key for later use to create lookup URIs 241 columnIndex = cursor.getColumnIndex(PhoneLookup.LOOKUP_KEY); 242 if (columnIndex != -1) { 243 info.lookupKeyOrNull = cursor.getString(columnIndex); 244 } 245 246 // Look for the person_id. 247 columnIndex = getColumnIndexForPersonId(contactRef, cursor); 248 if (columnIndex != -1) { 249 contactId = cursor.getLong(columnIndex); 250 if (contactId != 0 && !Contacts.isEnterpriseContactId(contactId)) { 251 info.contactIdOrZero = contactId; 252 Log.v(TAG, "==> got info.contactIdOrZero: " + info.contactIdOrZero); 253 } 254 } else { 255 // No valid columnIndex, so we can't look up person_id. 256 Log.v(TAG, "Couldn't find contactId column for " + contactRef); 257 // Watch out: this means that anything that depends on 258 // person_id will be broken (like contact photo lookups in 259 // the in-call UI, for example.) 260 } 261 262 // Display photo URI. 263 columnIndex = cursor.getColumnIndex(PhoneLookup.PHOTO_URI); 264 if ((columnIndex != -1) && (cursor.getString(columnIndex) != null)) { 265 info.contactDisplayPhotoUri = Uri.parse(cursor.getString(columnIndex)); 266 } else { 267 info.contactDisplayPhotoUri = null; 268 } 269 270 // look for the custom ringtone, create from the string stored 271 // in the database. 272 columnIndex = cursor.getColumnIndex(PhoneLookup.CUSTOM_RINGTONE); 273 if ((columnIndex != -1) && (cursor.getString(columnIndex) != null)) { 274 if (TextUtils.isEmpty(cursor.getString(columnIndex))) { 275 // make it consistent with frameworks/base/.../CallerInfo.java 276 info.contactRingtoneUri = Uri.EMPTY; 277 } else { 278 info.contactRingtoneUri = Uri.parse(cursor.getString(columnIndex)); 279 } 280 } else { 281 info.contactRingtoneUri = null; 282 } 283 284 // look for the send to voicemail flag, set it to true only 285 // under certain circumstances. 286 columnIndex = cursor.getColumnIndex(PhoneLookup.SEND_TO_VOICEMAIL); 287 info.shouldSendToVoicemail = (columnIndex != -1) && ((cursor.getInt(columnIndex)) == 1); 288 info.contactExists = true; 289 info.contactLookupResultType = ContactLookupResult.Type.LOCAL_CONTACT; 290 291 // Determine userType by directoryId and contactId 292 final String directory = 293 contactRef == null 294 ? null 295 : contactRef.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY); 296 Long directoryId = null; 297 if (directory != null) { 298 try { 299 directoryId = Long.parseLong(directory); 300 } catch (NumberFormatException e) { 301 // do nothing 302 } 303 } 304 info.userType = ContactsUtils.determineUserType(directoryId, contactId); 305 306 info.nameAlternative = 307 ContactInfoHelper.lookUpDisplayNameAlternative( 308 context, info.lookupKeyOrNull, info.userType, directoryId); 309 cursor.close(); 310 311 return info; 312 } 313 314 /** 315 * getCallerInfo given a URI, look up in the call-log database for the uri unique key. 316 * 317 * @param context the context used to get the ContentResolver 318 * @param contactRef the URI used to lookup caller id 319 * @return the CallerInfo which contains the caller id for the given number. The returned 320 * CallerInfo is null if no number is supplied. 321 */ getCallerInfo(Context context, Uri contactRef)322 private static CallerInfo getCallerInfo(Context context, Uri contactRef) { 323 324 return getCallerInfo( 325 context, 326 contactRef, 327 context.getContentResolver().query(contactRef, null, null, null, null)); 328 } 329 330 /** 331 * Performs another lookup if previous lookup fails and it's a SIP call and the peer's username is 332 * all numeric. Look up the username as it could be a PSTN number in the contact database. 333 * 334 * @param context the query context 335 * @param number the original phone number, could be a SIP URI 336 * @param previousResult the result of previous lookup 337 * @return previousResult if it's not the case 338 */ doSecondaryLookupIfNecessary( Context context, String number, CallerInfo previousResult)339 static CallerInfo doSecondaryLookupIfNecessary( 340 Context context, String number, CallerInfo previousResult) { 341 if (!previousResult.contactExists && PhoneNumberHelper.isUriNumber(number)) { 342 String username = PhoneNumberHelper.getUsernameFromUriNumber(number); 343 if (PhoneNumberUtils.isGlobalPhoneNumber(username)) { 344 previousResult = 345 getCallerInfo( 346 context, 347 Uri.withAppendedPath( 348 PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, Uri.encode(username))); 349 } 350 } 351 return previousResult; 352 } 353 354 // Accessors 355 normalize(String s)356 private static String normalize(String s) { 357 if (s == null || s.length() > 0) { 358 return s; 359 } else { 360 return null; 361 } 362 } 363 364 /** 365 * Returns the column index to use to find the "person_id" field in the specified cursor, based on 366 * the contact URI that was originally queried. 367 * 368 * <p>This is a helper function for the getCallerInfo() method that takes a Cursor. Looking up the 369 * person_id is nontrivial (compared to all the other CallerInfo fields) since the column we need 370 * to use depends on what query we originally ran. 371 * 372 * <p>Watch out: be sure to not do any database access in this method, since it's run from the UI 373 * thread (see comments below for more info.) 374 * 375 * @return the columnIndex to use (with cursor.getLong()) to get the person_id, or -1 if we 376 * couldn't figure out what colum to use. 377 * <p>TODO: Add a unittest for this method. (This is a little tricky to test, since we'll need 378 * a live contacts database to test against, preloaded with at least some phone numbers and 379 * SIP addresses. And we'll probably have to hardcode the column indexes we expect, so the 380 * test might break whenever the contacts schema changes. But we can at least make sure we 381 * handle all the URI patterns we claim to, and that the mime types match what we expect...) 382 */ getColumnIndexForPersonId(Uri contactRef, Cursor cursor)383 private static int getColumnIndexForPersonId(Uri contactRef, Cursor cursor) { 384 // TODO: This is pretty ugly now, see bug 2269240 for 385 // more details. The column to use depends upon the type of URL: 386 // - content://com.android.contacts/data/phones ==> use the "contact_id" column 387 // - content://com.android.contacts/phone_lookup ==> use the "_ID" column 388 // - content://com.android.contacts/data ==> use the "contact_id" column 389 // If it's none of the above, we leave columnIndex=-1 which means 390 // that the person_id field will be left unset. 391 // 392 // The logic here *used* to be based on the mime type of contactRef 393 // (for example Phone.CONTENT_ITEM_TYPE would tell us to use the 394 // RawContacts.CONTACT_ID column). But looking up the mime type requires 395 // a call to context.getContentResolver().getType(contactRef), which 396 // isn't safe to do from the UI thread since it can cause an ANR if 397 // the contacts provider is slow or blocked (like during a sync.) 398 // 399 // So instead, figure out the column to use for person_id by just 400 // looking at the URI itself. 401 402 Log.v(TAG, "- getColumnIndexForPersonId: contactRef URI = '" + contactRef + "'..."); 403 // Warning: Do not enable the following logging (due to ANR risk.) 404 // if (VDBG) Rlog.v(TAG, "- MIME type: " 405 // + context.getContentResolver().getType(contactRef)); 406 407 String url = contactRef.toString(); 408 String columnName = null; 409 if (url.startsWith("content://com.android.contacts/data/phones")) { 410 // Direct lookup in the Phone table. 411 // MIME type: Phone.CONTENT_ITEM_TYPE (= "vnd.android.cursor.item/phone_v2") 412 Log.v(TAG, "'data/phones' URI; using RawContacts.CONTACT_ID"); 413 columnName = RawContacts.CONTACT_ID; 414 } else if (url.startsWith("content://com.android.contacts/data")) { 415 // Direct lookup in the Data table. 416 // MIME type: Data.CONTENT_TYPE (= "vnd.android.cursor.dir/data") 417 Log.v(TAG, "'data' URI; using Data.CONTACT_ID"); 418 // (Note Data.CONTACT_ID and RawContacts.CONTACT_ID are equivalent.) 419 columnName = Data.CONTACT_ID; 420 } else if (url.startsWith("content://com.android.contacts/phone_lookup")) { 421 // Lookup in the PhoneLookup table, which provides "fuzzy matching" 422 // for phone numbers. 423 // MIME type: PhoneLookup.CONTENT_TYPE (= "vnd.android.cursor.dir/phone_lookup") 424 Log.v(TAG, "'phone_lookup' URI; using PhoneLookup._ID"); 425 columnName = PhoneLookup.CONTACT_ID; 426 } else { 427 Log.v(TAG, "Unexpected prefix for contactRef '" + url + "'"); 428 } 429 int columnIndex = (columnName != null) ? cursor.getColumnIndex(columnName) : -1; 430 Log.v( 431 TAG, 432 "==> Using column '" 433 + columnName 434 + "' (columnIndex = " 435 + columnIndex 436 + ") for person_id lookup..."); 437 return columnIndex; 438 } 439 440 /** @return true if the caller info is an emergency number. */ isEmergencyNumber()441 public boolean isEmergencyNumber() { 442 return isEmergency; 443 } 444 445 /** @return true if the caller info is a voicemail number. */ isVoiceMailNumber()446 public boolean isVoiceMailNumber() { 447 return isVoiceMail; 448 } 449 450 /** 451 * Mark this CallerInfo as an emergency call. 452 * 453 * @param context To lookup the localized 'Emergency Number' string. 454 * @return this instance. 455 */ markAsEmergency(Context context)456 /* package */ CallerInfo markAsEmergency(Context context) { 457 name = context.getString(R.string.emergency_number); 458 phoneNumber = null; 459 460 isEmergency = true; 461 return this; 462 } 463 464 /** 465 * Mark this CallerInfo as a voicemail call. The voicemail label is obtained from the telephony 466 * manager. Caller must hold the READ_PHONE_STATE permission otherwise the phoneNumber will be set 467 * to null. 468 * 469 * @return this instance. 470 */ markAsVoiceMail(Context context)471 /* package */ CallerInfo markAsVoiceMail(Context context) { 472 isVoiceMail = true; 473 474 try { 475 // For voicemail calls, we display the voice mail tag 476 // instead of the real phone number in the "number" 477 // field. 478 name = TelephonyManagerUtils.getVoiceMailAlphaTag(context); 479 phoneNumber = null; 480 } catch (SecurityException se) { 481 // Should never happen: if this process does not have 482 // permission to retrieve VM tag, it should not have 483 // permission to retrieve VM number and would not call 484 // this method. 485 // Leave phoneNumber untouched. 486 Log.e(TAG, "Cannot access VoiceMail.", se); 487 } 488 // TODO: There is no voicemail picture? 489 // photoResource = android.R.drawable.badge_voicemail; 490 return this; 491 } 492 493 /** 494 * Updates this CallerInfo's geoDescription field, based on the raw phone number in the 495 * phoneNumber field. 496 * 497 * <p>(Note that the various getCallerInfo() methods do *not* set the geoDescription 498 * automatically; you need to call this method explicitly to get it.) 499 * 500 * @param context the context used to look up the current locale / country 501 * @param fallbackNumber if this CallerInfo's phoneNumber field is empty, this specifies a 502 * fallback number to use instead. 503 */ updateGeoDescription(Context context, String fallbackNumber)504 public void updateGeoDescription(Context context, String fallbackNumber) { 505 String number = TextUtils.isEmpty(phoneNumber) ? fallbackNumber : phoneNumber; 506 geoDescription = PhoneNumberHelper.getGeoDescription(context, number, countryIso); 507 } 508 509 /** @return a string debug representation of this instance. */ 510 @Override toString()511 public String toString() { 512 // Warning: never check in this file with VERBOSE_DEBUG = true 513 // because that will result in PII in the system log. 514 final boolean VERBOSE_DEBUG = false; 515 516 if (VERBOSE_DEBUG) { 517 return new StringBuilder(384) 518 .append(super.toString() + " { ") 519 .append("\nname: " + name) 520 .append("\nphoneNumber: " + phoneNumber) 521 .append("\nnormalizedNumber: " + normalizedNumber) 522 .append("\forwardingNumber: " + forwardingNumber) 523 .append("\ngeoDescription: " + geoDescription) 524 .append("\ncnapName: " + cnapName) 525 .append("\nnumberPresentation: " + numberPresentation) 526 .append("\nnamePresentation: " + namePresentation) 527 .append("\ncontactExists: " + contactExists) 528 .append("\nphoneLabel: " + phoneLabel) 529 .append("\nnumberType: " + numberType) 530 .append("\nnumberLabel: " + numberLabel) 531 .append("\nphotoResource: " + photoResource) 532 .append("\ncontactIdOrZero: " + contactIdOrZero) 533 .append("\nneedUpdate: " + needUpdate) 534 .append("\ncontactRefUri: " + contactRefUri) 535 .append("\ncontactRingtoneUri: " + contactRingtoneUri) 536 .append("\ncontactDisplayPhotoUri: " + contactDisplayPhotoUri) 537 .append("\nshouldSendToVoicemail: " + shouldSendToVoicemail) 538 .append("\ncachedPhoto: " + cachedPhoto) 539 .append("\nisCachedPhotoCurrent: " + isCachedPhotoCurrent) 540 .append("\nemergency: " + isEmergency) 541 .append("\nvoicemail: " + isVoiceMail) 542 .append("\nuserType: " + userType) 543 .append(" }") 544 .toString(); 545 } else { 546 return new StringBuilder(128) 547 .append(super.toString() + " { ") 548 .append("name " + ((name == null) ? "null" : "non-null")) 549 .append(", phoneNumber " + ((phoneNumber == null) ? "null" : "non-null")) 550 .append(" }") 551 .toString(); 552 } 553 } 554 } 555