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