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