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