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