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