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