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