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