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