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