• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.incallui;
18 
19 import android.content.Context;
20 import android.database.Cursor;
21 import android.graphics.Bitmap;
22 import android.graphics.drawable.Drawable;
23 import android.net.Uri;
24 import android.provider.ContactsContract;
25 import android.provider.ContactsContract.CommonDataKinds.Phone;
26 import android.provider.ContactsContract.Contacts;
27 import android.provider.ContactsContract.Data;
28 import android.provider.ContactsContract.PhoneLookup;
29 import android.provider.ContactsContract.RawContacts;
30 import android.telephony.PhoneNumberUtils;
31 import android.text.TextUtils;
32 import com.android.contacts.common.ContactsUtils;
33 import com.android.contacts.common.ContactsUtils.UserType;
34 import com.android.contacts.common.util.TelephonyManagerUtils;
35 import com.android.dialer.logging.ContactLookupResult;
36 import com.android.dialer.phonenumbercache.ContactInfoHelper;
37 import com.android.dialer.phonenumberutil.PhoneNumberHelper;
38 
39 /**
40  * Looks up caller information for the given phone number. This is intermediate data and should NOT
41  * be used by any UI.
42  */
43 public class CallerInfo {
44 
45   private static final String TAG = "CallerInfo";
46 
47   private static final String[] DEFAULT_PHONELOOKUP_PROJECTION =
48       new String[] {
49         PhoneLookup.CONTACT_ID,
50         PhoneLookup.DISPLAY_NAME,
51         PhoneLookup.LOOKUP_KEY,
52         PhoneLookup.NUMBER,
53         PhoneLookup.NORMALIZED_NUMBER,
54         PhoneLookup.LABEL,
55         PhoneLookup.TYPE,
56         PhoneLookup.PHOTO_URI,
57         PhoneLookup.CUSTOM_RINGTONE,
58         PhoneLookup.SEND_TO_VOICEMAIL
59       };
60 
61   /**
62    * Please note that, any one of these member variables can be null, and any accesses to them
63    * should be prepared to handle such a case.
64    *
65    * <p>Also, it is implied that phoneNumber is more often populated than name is, (think of calls
66    * being dialed/received using numbers where names are not known to the device), so phoneNumber
67    * should serve as a dependable fallback when name is unavailable.
68    *
69    * <p>One other detail here is that this CallerInfo object reflects information found on a
70    * connection, it is an OUTPUT that serves mainly to display information to the user. In no way is
71    * this object used as input to make a connection, so we can choose to display whatever
72    * human-readable text makes sense to the user for a connection. This is especially relevant for
73    * the phone number field, since it is the one field that is most likely exposed to the user.
74    *
75    * <p>As an example: 1. User dials "911" 2. Device recognizes that this is an emergency number 3.
76    * We use the "Emergency Number" string instead of "911" in the phoneNumber field.
77    *
78    * <p>What we're really doing here is treating phoneNumber as an essential field here, NOT name.
79    * We're NOT always guaranteed to have a name for a connection, but the number should be
80    * displayable.
81    */
82   public String name;
83 
84   public String nameAlternative;
85   public String phoneNumber;
86   public String normalizedNumber;
87   public String forwardingNumber;
88   public String geoDescription;
89   boolean shouldShowGeoDescription;
90   public String cnapName;
91   public int numberPresentation;
92   public int namePresentation;
93   public boolean contactExists;
94   public ContactLookupResult.Type contactLookupResultType = ContactLookupResult.Type.NOT_FOUND;
95   public String phoneLabel;
96   /* Split up the phoneLabel into number type and label name */
97   public int numberType;
98   public String numberLabel;
99   public int photoResource;
100   // Contact ID, which will be 0 if a contact comes from the corp CP2.
101   public long contactIdOrZero;
102   public String lookupKeyOrNull;
103   public boolean needUpdate;
104   public Uri contactRefUri;
105   public @UserType long userType;
106   /**
107    * Contact display photo URI. If a contact has no display photo but a thumbnail, it'll be the
108    * thumbnail URI instead.
109    */
110   public Uri contactDisplayPhotoUri;
111   // fields to hold individual contact preference data,
112   // including the send to voicemail flag and the ringtone
113   // uri reference.
114   public Uri contactRingtoneUri;
115   public boolean shouldSendToVoicemail;
116   /**
117    * Drawable representing the caller image. This is essentially a cache for the image data tied
118    * into the connection / callerinfo object.
119    *
120    * <p>This might be a high resolution picture which is more suitable for full-screen image view
121    * than for smaller icons used in some kinds of notifications.
122    *
123    * <p>The {@link #isCachedPhotoCurrent} flag indicates if the image data needs to be reloaded.
124    */
125   public Drawable cachedPhoto;
126   /**
127    * Bitmap representing the caller image which has possibly lower resolution than {@link
128    * #cachedPhoto} and thus more suitable for icons (like notification icons).
129    *
130    * <p>In usual cases this is just down-scaled image of {@link #cachedPhoto}. If the down-scaling
131    * fails, this will just become null.
132    *
133    * <p>The {@link #isCachedPhotoCurrent} flag indicates if the image data needs to be reloaded.
134    */
135   public Bitmap cachedPhotoIcon;
136   /**
137    * Boolean which indicates if {@link #cachedPhoto} and {@link #cachedPhotoIcon} is fresh enough.
138    * If it is false, those images aren't pointing to valid objects.
139    */
140   public boolean isCachedPhotoCurrent;
141   /**
142    * String which holds the call subject sent as extra from the lower layers for this call. This is
143    * used to display the no-caller ID reason for restricted/unknown number presentation.
144    */
145   public String callSubject;
146 
147   public String countryIso;
148 
149   private boolean isEmergency;
150   private boolean isVoiceMail;
151 
CallerInfo()152   public CallerInfo() {
153     // TODO: Move all the basic initialization here?
154     isEmergency = false;
155     isVoiceMail = false;
156     userType = ContactsUtils.USER_TYPE_CURRENT;
157   }
158 
getDefaultPhoneLookupProjection()159   static String[] getDefaultPhoneLookupProjection() {
160     return DEFAULT_PHONELOOKUP_PROJECTION;
161   }
162 
163   /**
164    * getCallerInfo given a Cursor.
165    *
166    * @param context the context used to retrieve string constants
167    * @param contactRef the URI to attach to this CallerInfo object
168    * @param cursor the first object in the cursor is used to build the CallerInfo object.
169    * @return the CallerInfo which contains the caller id for the given number. The returned
170    *     CallerInfo is null if no number is supplied.
171    */
getCallerInfo(Context context, Uri contactRef, Cursor cursor)172   public static CallerInfo getCallerInfo(Context context, Uri contactRef, Cursor cursor) {
173     CallerInfo info = new CallerInfo();
174     info.cachedPhoto = null;
175     info.contactExists = false;
176     info.contactRefUri = contactRef;
177     info.isCachedPhotoCurrent = false;
178     info.name = null;
179     info.needUpdate = false;
180     info.numberLabel = null;
181     info.numberType = 0;
182     info.phoneLabel = null;
183     info.photoResource = 0;
184     info.userType = ContactsUtils.USER_TYPE_CURRENT;
185 
186     Log.v(TAG, "getCallerInfo() based on cursor...");
187 
188     if (cursor == null || !cursor.moveToFirst()) {
189       return info;
190     }
191 
192     // TODO: photo_id is always available but not taken
193     // care of here. Maybe we should store it in the
194     // CallerInfo object as well.
195 
196     long contactId = 0L;
197     int columnIndex;
198 
199     // Look for the number
200     columnIndex = cursor.getColumnIndex(PhoneLookup.NUMBER);
201     if (columnIndex != -1) {
202       // The Contacts provider ignores special characters in phone numbers when searching for a
203       // contact. For example, number "123" is considered a match with a contact with number "#123".
204       // We need to check whether the result contains a number that truly matches the query and move
205       // the cursor to that position before filling in the fields in CallerInfo.
206       boolean hasNumberMatch =
207           PhoneNumberHelper.updateCursorToMatchContactLookupUri(cursor, columnIndex, contactRef);
208       if (hasNumberMatch) {
209         info.phoneNumber = cursor.getString(columnIndex);
210       } else {
211         return info;
212       }
213     }
214 
215     // Look for the name
216     columnIndex = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME);
217     if (columnIndex != -1) {
218       info.name = normalize(cursor.getString(columnIndex));
219     }
220 
221     // Look for the normalized number
222     columnIndex = cursor.getColumnIndex(PhoneLookup.NORMALIZED_NUMBER);
223     if (columnIndex != -1) {
224       info.normalizedNumber = cursor.getString(columnIndex);
225     }
226 
227     // Look for the label/type combo
228     columnIndex = cursor.getColumnIndex(PhoneLookup.LABEL);
229     if (columnIndex != -1) {
230       int typeColumnIndex = cursor.getColumnIndex(PhoneLookup.TYPE);
231       if (typeColumnIndex != -1) {
232         info.numberType = cursor.getInt(typeColumnIndex);
233         info.numberLabel = cursor.getString(columnIndex);
234         info.phoneLabel =
235             Phone.getTypeLabel(context.getResources(), info.numberType, info.numberLabel)
236                 .toString();
237       }
238     }
239 
240     // cache the lookup key for later use to create lookup URIs
241     columnIndex = cursor.getColumnIndex(PhoneLookup.LOOKUP_KEY);
242     if (columnIndex != -1) {
243       info.lookupKeyOrNull = cursor.getString(columnIndex);
244     }
245 
246     // Look for the person_id.
247     columnIndex = getColumnIndexForPersonId(contactRef, cursor);
248     if (columnIndex != -1) {
249       contactId = cursor.getLong(columnIndex);
250       if (contactId != 0 && !Contacts.isEnterpriseContactId(contactId)) {
251         info.contactIdOrZero = contactId;
252         Log.v(TAG, "==> got info.contactIdOrZero: " + info.contactIdOrZero);
253       }
254     } else {
255       // No valid columnIndex, so we can't look up person_id.
256       Log.v(TAG, "Couldn't find contactId column for " + contactRef);
257       // Watch out: this means that anything that depends on
258       // person_id will be broken (like contact photo lookups in
259       // the in-call UI, for example.)
260     }
261 
262     // Display photo URI.
263     columnIndex = cursor.getColumnIndex(PhoneLookup.PHOTO_URI);
264     if ((columnIndex != -1) && (cursor.getString(columnIndex) != null)) {
265       info.contactDisplayPhotoUri = Uri.parse(cursor.getString(columnIndex));
266     } else {
267       info.contactDisplayPhotoUri = null;
268     }
269 
270     // look for the custom ringtone, create from the string stored
271     // in the database.
272     columnIndex = cursor.getColumnIndex(PhoneLookup.CUSTOM_RINGTONE);
273     if ((columnIndex != -1) && (cursor.getString(columnIndex) != null)) {
274       if (TextUtils.isEmpty(cursor.getString(columnIndex))) {
275         // make it consistent with frameworks/base/.../CallerInfo.java
276         info.contactRingtoneUri = Uri.EMPTY;
277       } else {
278         info.contactRingtoneUri = Uri.parse(cursor.getString(columnIndex));
279       }
280     } else {
281       info.contactRingtoneUri = null;
282     }
283 
284     // look for the send to voicemail flag, set it to true only
285     // under certain circumstances.
286     columnIndex = cursor.getColumnIndex(PhoneLookup.SEND_TO_VOICEMAIL);
287     info.shouldSendToVoicemail = (columnIndex != -1) && ((cursor.getInt(columnIndex)) == 1);
288     info.contactExists = true;
289     info.contactLookupResultType = ContactLookupResult.Type.LOCAL_CONTACT;
290 
291     // Determine userType by directoryId and contactId
292     final String directory =
293         contactRef == null
294             ? null
295             : contactRef.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
296     Long directoryId = null;
297     if (directory != null) {
298       try {
299         directoryId = Long.parseLong(directory);
300       } catch (NumberFormatException e) {
301         // do nothing
302       }
303     }
304     info.userType = ContactsUtils.determineUserType(directoryId, contactId);
305 
306     info.nameAlternative =
307         ContactInfoHelper.lookUpDisplayNameAlternative(
308             context, info.lookupKeyOrNull, info.userType, directoryId);
309     cursor.close();
310 
311     return info;
312   }
313 
314   /**
315    * getCallerInfo given a URI, look up in the call-log database for the uri unique key.
316    *
317    * @param context the context used to get the ContentResolver
318    * @param contactRef the URI used to lookup caller id
319    * @return the CallerInfo which contains the caller id for the given number. The returned
320    *     CallerInfo is null if no number is supplied.
321    */
getCallerInfo(Context context, Uri contactRef)322   private static CallerInfo getCallerInfo(Context context, Uri contactRef) {
323 
324     return getCallerInfo(
325         context,
326         contactRef,
327         context.getContentResolver().query(contactRef, null, null, null, null));
328   }
329 
330   /**
331    * Performs another lookup if previous lookup fails and it's a SIP call and the peer's username is
332    * all numeric. Look up the username as it could be a PSTN number in the contact database.
333    *
334    * @param context the query context
335    * @param number the original phone number, could be a SIP URI
336    * @param previousResult the result of previous lookup
337    * @return previousResult if it's not the case
338    */
doSecondaryLookupIfNecessary( Context context, String number, CallerInfo previousResult)339   static CallerInfo doSecondaryLookupIfNecessary(
340       Context context, String number, CallerInfo previousResult) {
341     if (!previousResult.contactExists && PhoneNumberHelper.isUriNumber(number)) {
342       String username = PhoneNumberHelper.getUsernameFromUriNumber(number);
343       if (PhoneNumberUtils.isGlobalPhoneNumber(username)) {
344         previousResult =
345             getCallerInfo(
346                 context,
347                 Uri.withAppendedPath(
348                     PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, Uri.encode(username)));
349       }
350     }
351     return previousResult;
352   }
353 
354   // Accessors
355 
normalize(String s)356   private static String normalize(String s) {
357     if (s == null || s.length() > 0) {
358       return s;
359     } else {
360       return null;
361     }
362   }
363 
364   /**
365    * Returns the column index to use to find the "person_id" field in the specified cursor, based on
366    * the contact URI that was originally queried.
367    *
368    * <p>This is a helper function for the getCallerInfo() method that takes a Cursor. Looking up the
369    * person_id is nontrivial (compared to all the other CallerInfo fields) since the column we need
370    * to use depends on what query we originally ran.
371    *
372    * <p>Watch out: be sure to not do any database access in this method, since it's run from the UI
373    * thread (see comments below for more info.)
374    *
375    * @return the columnIndex to use (with cursor.getLong()) to get the person_id, or -1 if we
376    *     couldn't figure out what colum to use.
377    *     <p>TODO: Add a unittest for this method. (This is a little tricky to test, since we'll need
378    *     a live contacts database to test against, preloaded with at least some phone numbers and
379    *     SIP addresses. And we'll probably have to hardcode the column indexes we expect, so the
380    *     test might break whenever the contacts schema changes. But we can at least make sure we
381    *     handle all the URI patterns we claim to, and that the mime types match what we expect...)
382    */
getColumnIndexForPersonId(Uri contactRef, Cursor cursor)383   private static int getColumnIndexForPersonId(Uri contactRef, Cursor cursor) {
384     // TODO: This is pretty ugly now, see bug 2269240 for
385     // more details. The column to use depends upon the type of URL:
386     // - content://com.android.contacts/data/phones ==> use the "contact_id" column
387     // - content://com.android.contacts/phone_lookup ==> use the "_ID" column
388     // - content://com.android.contacts/data ==> use the "contact_id" column
389     // If it's none of the above, we leave columnIndex=-1 which means
390     // that the person_id field will be left unset.
391     //
392     // The logic here *used* to be based on the mime type of contactRef
393     // (for example Phone.CONTENT_ITEM_TYPE would tell us to use the
394     // RawContacts.CONTACT_ID column).  But looking up the mime type requires
395     // a call to context.getContentResolver().getType(contactRef), which
396     // isn't safe to do from the UI thread since it can cause an ANR if
397     // the contacts provider is slow or blocked (like during a sync.)
398     //
399     // So instead, figure out the column to use for person_id by just
400     // looking at the URI itself.
401 
402     Log.v(TAG, "- getColumnIndexForPersonId: contactRef URI = '" + contactRef + "'...");
403     // Warning: Do not enable the following logging (due to ANR risk.)
404     // if (VDBG) Rlog.v(TAG, "- MIME type: "
405     //                 + context.getContentResolver().getType(contactRef));
406 
407     String url = contactRef.toString();
408     String columnName = null;
409     if (url.startsWith("content://com.android.contacts/data/phones")) {
410       // Direct lookup in the Phone table.
411       // MIME type: Phone.CONTENT_ITEM_TYPE (= "vnd.android.cursor.item/phone_v2")
412       Log.v(TAG, "'data/phones' URI; using RawContacts.CONTACT_ID");
413       columnName = RawContacts.CONTACT_ID;
414     } else if (url.startsWith("content://com.android.contacts/data")) {
415       // Direct lookup in the Data table.
416       // MIME type: Data.CONTENT_TYPE (= "vnd.android.cursor.dir/data")
417       Log.v(TAG, "'data' URI; using Data.CONTACT_ID");
418       // (Note Data.CONTACT_ID and RawContacts.CONTACT_ID are equivalent.)
419       columnName = Data.CONTACT_ID;
420     } else if (url.startsWith("content://com.android.contacts/phone_lookup")) {
421       // Lookup in the PhoneLookup table, which provides "fuzzy matching"
422       // for phone numbers.
423       // MIME type: PhoneLookup.CONTENT_TYPE (= "vnd.android.cursor.dir/phone_lookup")
424       Log.v(TAG, "'phone_lookup' URI; using PhoneLookup._ID");
425       columnName = PhoneLookup.CONTACT_ID;
426     } else {
427       Log.v(TAG, "Unexpected prefix for contactRef '" + url + "'");
428     }
429     int columnIndex = (columnName != null) ? cursor.getColumnIndex(columnName) : -1;
430     Log.v(
431         TAG,
432         "==> Using column '"
433             + columnName
434             + "' (columnIndex = "
435             + columnIndex
436             + ") for person_id lookup...");
437     return columnIndex;
438   }
439 
440   /** @return true if the caller info is an emergency number. */
isEmergencyNumber()441   public boolean isEmergencyNumber() {
442     return isEmergency;
443   }
444 
445   /** @return true if the caller info is a voicemail number. */
isVoiceMailNumber()446   public boolean isVoiceMailNumber() {
447     return isVoiceMail;
448   }
449 
450   /**
451    * Mark this CallerInfo as an emergency call.
452    *
453    * @param context To lookup the localized 'Emergency Number' string.
454    * @return this instance.
455    */
markAsEmergency(Context context)456   /* package */ CallerInfo markAsEmergency(Context context) {
457     name = context.getString(R.string.emergency_number);
458     phoneNumber = null;
459 
460     isEmergency = true;
461     return this;
462   }
463 
464   /**
465    * Mark this CallerInfo as a voicemail call. The voicemail label is obtained from the telephony
466    * manager. Caller must hold the READ_PHONE_STATE permission otherwise the phoneNumber will be set
467    * to null.
468    *
469    * @return this instance.
470    */
markAsVoiceMail(Context context)471   /* package */ CallerInfo markAsVoiceMail(Context context) {
472     isVoiceMail = true;
473 
474     try {
475       // For voicemail calls, we display the voice mail tag
476       // instead of the real phone number in the "number"
477       // field.
478       name = TelephonyManagerUtils.getVoiceMailAlphaTag(context);
479       phoneNumber = null;
480     } catch (SecurityException se) {
481       // Should never happen: if this process does not have
482       // permission to retrieve VM tag, it should not have
483       // permission to retrieve VM number and would not call
484       // this method.
485       // Leave phoneNumber untouched.
486       Log.e(TAG, "Cannot access VoiceMail.", se);
487     }
488     // TODO: There is no voicemail picture?
489     // photoResource = android.R.drawable.badge_voicemail;
490     return this;
491   }
492 
493   /**
494    * Updates this CallerInfo's geoDescription field, based on the raw phone number in the
495    * phoneNumber field.
496    *
497    * <p>(Note that the various getCallerInfo() methods do *not* set the geoDescription
498    * automatically; you need to call this method explicitly to get it.)
499    *
500    * @param context the context used to look up the current locale / country
501    * @param fallbackNumber if this CallerInfo's phoneNumber field is empty, this specifies a
502    *     fallback number to use instead.
503    */
updateGeoDescription(Context context, String fallbackNumber)504   public void updateGeoDescription(Context context, String fallbackNumber) {
505     String number = TextUtils.isEmpty(phoneNumber) ? fallbackNumber : phoneNumber;
506     geoDescription = PhoneNumberHelper.getGeoDescription(context, number, countryIso);
507   }
508 
509   /** @return a string debug representation of this instance. */
510   @Override
toString()511   public String toString() {
512     // Warning: never check in this file with VERBOSE_DEBUG = true
513     // because that will result in PII in the system log.
514     final boolean VERBOSE_DEBUG = false;
515 
516     if (VERBOSE_DEBUG) {
517       return new StringBuilder(384)
518           .append(super.toString() + " { ")
519           .append("\nname: " + name)
520           .append("\nphoneNumber: " + phoneNumber)
521           .append("\nnormalizedNumber: " + normalizedNumber)
522           .append("\forwardingNumber: " + forwardingNumber)
523           .append("\ngeoDescription: " + geoDescription)
524           .append("\ncnapName: " + cnapName)
525           .append("\nnumberPresentation: " + numberPresentation)
526           .append("\nnamePresentation: " + namePresentation)
527           .append("\ncontactExists: " + contactExists)
528           .append("\nphoneLabel: " + phoneLabel)
529           .append("\nnumberType: " + numberType)
530           .append("\nnumberLabel: " + numberLabel)
531           .append("\nphotoResource: " + photoResource)
532           .append("\ncontactIdOrZero: " + contactIdOrZero)
533           .append("\nneedUpdate: " + needUpdate)
534           .append("\ncontactRefUri: " + contactRefUri)
535           .append("\ncontactRingtoneUri: " + contactRingtoneUri)
536           .append("\ncontactDisplayPhotoUri: " + contactDisplayPhotoUri)
537           .append("\nshouldSendToVoicemail: " + shouldSendToVoicemail)
538           .append("\ncachedPhoto: " + cachedPhoto)
539           .append("\nisCachedPhotoCurrent: " + isCachedPhotoCurrent)
540           .append("\nemergency: " + isEmergency)
541           .append("\nvoicemail: " + isVoiceMail)
542           .append("\nuserType: " + userType)
543           .append(" }")
544           .toString();
545     } else {
546       return new StringBuilder(128)
547           .append(super.toString() + " { ")
548           .append("name " + ((name == null) ? "null" : "non-null"))
549           .append(", phoneNumber " + ((phoneNumber == null) ? "null" : "non-null"))
550           .append(" }")
551           .toString();
552     }
553   }
554 }
555