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