1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  * in compliance with the License. You may obtain a copy of the License at
6  *
7  * http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the License
10  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11  * or implied. See the License for the specific language governing permissions and limitations under
12  * the License.
13  */
14 
15 package com.android.dialer.calllog;
16 
17 import android.content.ContentValues;
18 import android.content.Context;
19 import android.database.Cursor;
20 import android.database.sqlite.SQLiteFullException;
21 import android.net.Uri;
22 import android.provider.CallLog.Calls;
23 import android.provider.ContactsContract;
24 import android.provider.ContactsContract.CommonDataKinds.Phone;
25 import android.provider.ContactsContract.Contacts;
26 import android.provider.ContactsContract.DisplayNameSources;
27 import android.provider.ContactsContract.PhoneLookup;
28 import android.support.annotation.Nullable;
29 import android.telephony.PhoneNumberUtils;
30 import android.text.TextUtils;
31 import android.util.Log;
32 
33 import com.android.contacts.common.ContactsUtils;
34 import com.android.contacts.common.ContactsUtils.UserType;
35 import com.android.contacts.common.compat.CompatUtils;
36 import com.android.contacts.common.util.Constants;
37 import com.android.contacts.common.util.PermissionsUtil;
38 import com.android.contacts.common.util.PhoneNumberHelper;
39 import com.android.contacts.common.util.UriUtils;
40 import com.android.dialer.compat.DialerCompatUtils;
41 import com.android.dialer.service.CachedNumberLookupService;
42 import com.android.dialer.service.CachedNumberLookupService.CachedContactInfo;
43 import com.android.dialer.util.TelecomUtil;
44 import com.android.dialerbind.ObjectFactory;
45 
46 import org.json.JSONException;
47 import org.json.JSONObject;
48 
49 /**
50  * Utility class to look up the contact information for a given number.
51  */
52 public class ContactInfoHelper {
53     private static final String TAG = ContactInfoHelper.class.getSimpleName();
54 
55     private final Context mContext;
56     private final String mCurrentCountryIso;
57 
58     private static final CachedNumberLookupService mCachedNumberLookupService =
59             ObjectFactory.newCachedNumberLookupService();
60 
ContactInfoHelper(Context context, String currentCountryIso)61     public ContactInfoHelper(Context context, String currentCountryIso) {
62         mContext = context;
63         mCurrentCountryIso = currentCountryIso;
64     }
65 
66     /**
67      * Returns the contact information for the given number.
68      * <p>
69      * If the number does not match any contact, returns a contact info containing only the number
70      * and the formatted number.
71      * <p>
72      * If an error occurs during the lookup, it returns null.
73      *
74      * @param number the number to look up
75      * @param countryIso the country associated with this number
76      */
77     @Nullable
lookupNumber(String number, String countryIso)78     public ContactInfo lookupNumber(String number, String countryIso) {
79         if (TextUtils.isEmpty(number)) {
80             return null;
81         }
82 
83         ContactInfo info;
84 
85         if (PhoneNumberHelper.isUriNumber(number)) {
86             // The number is a SIP address..
87             info = lookupContactFromUri(getContactInfoLookupUri(number), true);
88             if (info == null || info == ContactInfo.EMPTY) {
89                 // If lookup failed, check if the "username" of the SIP address is a phone number.
90                 String username = PhoneNumberHelper.getUsernameFromUriNumber(number);
91                 if (PhoneNumberUtils.isGlobalPhoneNumber(username)) {
92                     info = queryContactInfoForPhoneNumber(username, countryIso, true);
93                 }
94             }
95         } else {
96             // Look for a contact that has the given phone number.
97             info = queryContactInfoForPhoneNumber(number, countryIso, false);
98         }
99 
100         final ContactInfo updatedInfo;
101         if (info == null) {
102             // The lookup failed.
103             updatedInfo = null;
104         } else {
105             // If we did not find a matching contact, generate an empty contact info for the number.
106             if (info == ContactInfo.EMPTY) {
107                 // Did not find a matching contact.
108                 updatedInfo = new ContactInfo();
109                 updatedInfo.number = number;
110                 updatedInfo.formattedNumber = formatPhoneNumber(number, null, countryIso);
111                 updatedInfo.normalizedNumber = PhoneNumberUtils.formatNumberToE164(
112                         number, countryIso);
113                 updatedInfo.lookupUri = createTemporaryContactUri(updatedInfo.formattedNumber);
114             } else {
115                 updatedInfo = info;
116             }
117         }
118         return updatedInfo;
119     }
120 
121     /**
122      * Creates a JSON-encoded lookup uri for a unknown number without an associated contact
123      *
124      * @param number - Unknown phone number
125      * @return JSON-encoded URI that can be used to perform a lookup when clicking on the quick
126      *         contact card.
127      */
createTemporaryContactUri(String number)128     private static Uri createTemporaryContactUri(String number) {
129         try {
130             final JSONObject contactRows = new JSONObject().put(Phone.CONTENT_ITEM_TYPE,
131                     new JSONObject().put(Phone.NUMBER, number).put(Phone.TYPE, Phone.TYPE_CUSTOM));
132 
133             final String jsonString = new JSONObject().put(Contacts.DISPLAY_NAME, number)
134                     .put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.PHONE)
135                     .put(Contacts.CONTENT_ITEM_TYPE, contactRows).toString();
136 
137             return Contacts.CONTENT_LOOKUP_URI
138                     .buildUpon()
139                     .appendPath(Constants.LOOKUP_URI_ENCODED)
140                     .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
141                             String.valueOf(Long.MAX_VALUE))
142                     .encodedFragment(jsonString)
143                     .build();
144         } catch (JSONException e) {
145             return null;
146         }
147     }
148 
149     /**
150      * Looks up a contact using the given URI.
151      * <p>
152      * It returns null if an error occurs, {@link ContactInfo#EMPTY} if no matching contact is
153      * found, or the {@link ContactInfo} for the given contact.
154      * <p>
155      * The {@link ContactInfo#formattedNumber} field is always set to {@code null} in the returned
156      * value.
157      */
lookupContactFromUri(Uri uri, boolean isSip)158     ContactInfo lookupContactFromUri(Uri uri, boolean isSip) {
159         if (uri == null) {
160             return null;
161         }
162         if (!PermissionsUtil.hasContactsPermissions(mContext)) {
163             return ContactInfo.EMPTY;
164         }
165 
166         Cursor phoneLookupCursor = null;
167         try {
168             String[] projection = PhoneQuery.getPhoneLookupProjection(uri);
169             phoneLookupCursor = mContext.getContentResolver().query(uri, projection, null, null,
170                     null);
171         } catch (NullPointerException e) {
172             // Trap NPE from pre-N CP2
173             return null;
174         }
175         if (phoneLookupCursor == null) {
176             return null;
177         }
178 
179         try {
180             if (!phoneLookupCursor.moveToFirst()) {
181                 return ContactInfo.EMPTY;
182             }
183             String lookupKey = phoneLookupCursor.getString(PhoneQuery.LOOKUP_KEY);
184             ContactInfo contactInfo = createPhoneLookupContactInfo(phoneLookupCursor, lookupKey);
185             contactInfo.nameAlternative = lookUpDisplayNameAlternative(mContext, lookupKey,
186                     contactInfo.userType);
187             return contactInfo;
188         } finally {
189             phoneLookupCursor.close();
190         }
191     }
192 
createPhoneLookupContactInfo(Cursor phoneLookupCursor, String lookupKey)193     private ContactInfo createPhoneLookupContactInfo(Cursor phoneLookupCursor, String lookupKey) {
194         ContactInfo info = new ContactInfo();
195         info.lookupKey = lookupKey;
196         info.lookupUri = Contacts.getLookupUri(phoneLookupCursor.getLong(PhoneQuery.PERSON_ID),
197                 lookupKey);
198         info.name = phoneLookupCursor.getString(PhoneQuery.NAME);
199         info.type = phoneLookupCursor.getInt(PhoneQuery.PHONE_TYPE);
200         info.label = phoneLookupCursor.getString(PhoneQuery.LABEL);
201         info.number = phoneLookupCursor.getString(PhoneQuery.MATCHED_NUMBER);
202         info.normalizedNumber = phoneLookupCursor.getString(PhoneQuery.NORMALIZED_NUMBER);
203         info.photoId = phoneLookupCursor.getLong(PhoneQuery.PHOTO_ID);
204         info.photoUri = UriUtils.parseUriOrNull(phoneLookupCursor.getString(PhoneQuery.PHOTO_URI));
205         info.formattedNumber = null;
206         info.userType = ContactsUtils.determineUserType(null,
207                 phoneLookupCursor.getLong(PhoneQuery.PERSON_ID));
208 
209         return info;
210     }
211 
lookUpDisplayNameAlternative(Context context, String lookupKey, @UserType long userType)212     public static String lookUpDisplayNameAlternative(Context context, String lookupKey,
213             @UserType long userType) {
214         // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed.
215         if (lookupKey == null || userType == ContactsUtils.USER_TYPE_WORK) {
216             return null;
217         }
218         final Uri uri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey);
219         Cursor cursor = null;
220         try {
221             cursor = context.getContentResolver().query(uri,
222                     PhoneQuery.DISPLAY_NAME_ALTERNATIVE_PROJECTION, null, null, null);
223 
224             if (cursor != null && cursor.moveToFirst()) {
225                 return cursor.getString(PhoneQuery.NAME_ALTERNATIVE);
226             }
227         } catch (IllegalArgumentException e) {
228             // Avoid dialer crash when lookup key is not valid
229         } finally {
230             if (cursor != null) {
231                 cursor.close();
232             }
233         }
234 
235         return null;
236     }
237 
238     /**
239      * Determines the contact information for the given phone number.
240      * <p>
241      * It returns the contact info if found.
242      * <p>
243      * If no contact corresponds to the given phone number, returns {@link ContactInfo#EMPTY}.
244      * <p>
245      * If the lookup fails for some other reason, it returns null.
246      */
queryContactInfoForPhoneNumber(String number, String countryIso, boolean isSip)247     private ContactInfo queryContactInfoForPhoneNumber(String number, String countryIso,
248                                                        boolean isSip) {
249         if (TextUtils.isEmpty(number)) {
250             return null;
251         }
252 
253         ContactInfo info = lookupContactFromUri(getContactInfoLookupUri(number), isSip);
254         if (info != null && info != ContactInfo.EMPTY) {
255             info.formattedNumber = formatPhoneNumber(number, null, countryIso);
256         } else if (mCachedNumberLookupService != null) {
257             CachedContactInfo cacheInfo =
258                     mCachedNumberLookupService.lookupCachedContactFromNumber(mContext, number);
259             if (cacheInfo != null) {
260                 info = cacheInfo.getContactInfo().isBadData ? null : cacheInfo.getContactInfo();
261             } else {
262                 info = null;
263             }
264         }
265         return info;
266     }
267 
268     /**
269      * Format the given phone number
270      *
271      * @param number the number to be formatted.
272      * @param normalizedNumber the normalized number of the given number.
273      * @param countryIso the ISO 3166-1 two letters country code, the country's convention will be
274      *        used to format the number if the normalized phone is null.
275      *
276      * @return the formatted number, or the given number if it was formatted.
277      */
formatPhoneNumber(String number, String normalizedNumber, String countryIso)278     private String formatPhoneNumber(String number, String normalizedNumber, String countryIso) {
279         if (TextUtils.isEmpty(number)) {
280             return "";
281         }
282         // If "number" is really a SIP address, don't try to do any formatting at all.
283         if (PhoneNumberHelper.isUriNumber(number)) {
284             return number;
285         }
286         if (TextUtils.isEmpty(countryIso)) {
287             countryIso = mCurrentCountryIso;
288         }
289         return PhoneNumberUtils.formatNumber(number, normalizedNumber, countryIso);
290     }
291 
292     /**
293      * Stores differences between the updated contact info and the current call log contact info.
294      *
295      * @param number The number of the contact.
296      * @param countryIso The country associated with this number.
297      * @param updatedInfo The updated contact info.
298      * @param callLogInfo The call log entry's current contact info.
299      */
updateCallLogContactInfo(String number, String countryIso, ContactInfo updatedInfo, ContactInfo callLogInfo)300     public void updateCallLogContactInfo(String number, String countryIso, ContactInfo updatedInfo,
301             ContactInfo callLogInfo) {
302         if (!PermissionsUtil.hasPermission(mContext, android.Manifest.permission.WRITE_CALL_LOG)) {
303             return;
304         }
305 
306         final ContentValues values = new ContentValues();
307         boolean needsUpdate = false;
308 
309         if (callLogInfo != null) {
310             if (!TextUtils.equals(updatedInfo.name, callLogInfo.name)) {
311                 values.put(Calls.CACHED_NAME, updatedInfo.name);
312                 needsUpdate = true;
313             }
314 
315             if (updatedInfo.type != callLogInfo.type) {
316                 values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
317                 needsUpdate = true;
318             }
319 
320             if (!TextUtils.equals(updatedInfo.label, callLogInfo.label)) {
321                 values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
322                 needsUpdate = true;
323             }
324 
325             if (!UriUtils.areEqual(updatedInfo.lookupUri, callLogInfo.lookupUri)) {
326                 values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
327                 needsUpdate = true;
328             }
329 
330             // Only replace the normalized number if the new updated normalized number isn't empty.
331             if (!TextUtils.isEmpty(updatedInfo.normalizedNumber) &&
332                     !TextUtils.equals(updatedInfo.normalizedNumber, callLogInfo.normalizedNumber)) {
333                 values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
334                 needsUpdate = true;
335             }
336 
337             if (!TextUtils.equals(updatedInfo.number, callLogInfo.number)) {
338                 values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
339                 needsUpdate = true;
340             }
341 
342             if (updatedInfo.photoId != callLogInfo.photoId) {
343                 values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
344                 needsUpdate = true;
345             }
346 
347             final Uri updatedPhotoUriContactsOnly =
348                     UriUtils.nullForNonContactsUri(updatedInfo.photoUri);
349             if (DialerCompatUtils.isCallsCachedPhotoUriCompatible() &&
350                     !UriUtils.areEqual(updatedPhotoUriContactsOnly, callLogInfo.photoUri)) {
351                 values.put(Calls.CACHED_PHOTO_URI,
352                         UriUtils.uriToString(updatedPhotoUriContactsOnly));
353                 needsUpdate = true;
354             }
355 
356             if (!TextUtils.equals(updatedInfo.formattedNumber, callLogInfo.formattedNumber)) {
357                 values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
358                 needsUpdate = true;
359             }
360         } else {
361             // No previous values, store all of them.
362             values.put(Calls.CACHED_NAME, updatedInfo.name);
363             values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
364             values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
365             values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
366             values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
367             values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
368             values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
369             if (DialerCompatUtils.isCallsCachedPhotoUriCompatible()) {
370                 values.put(Calls.CACHED_PHOTO_URI, UriUtils.uriToString(
371                         UriUtils.nullForNonContactsUri(updatedInfo.photoUri)));
372             }
373             values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
374             needsUpdate = true;
375         }
376 
377         if (!needsUpdate) {
378             return;
379         }
380 
381         try {
382             if (countryIso == null) {
383                 mContext.getContentResolver().update(
384                         TelecomUtil.getCallLogUri(mContext),
385                         values,
386                         Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " IS NULL",
387                         new String[]{ number });
388             } else {
389                 mContext.getContentResolver().update(
390                         TelecomUtil.getCallLogUri(mContext),
391                         values,
392                         Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " = ?",
393                         new String[]{ number, countryIso });
394             }
395         } catch (SQLiteFullException e) {
396             Log.e(TAG, "Unable to update contact info in call log db", e);
397         }
398     }
399 
getContactInfoLookupUri(String number)400     public static Uri getContactInfoLookupUri(String number) {
401         return getContactInfoLookupUri(number, -1);
402     }
403 
getContactInfoLookupUri(String number, long directoryId)404     public static Uri getContactInfoLookupUri(String number, long directoryId) {
405         // Get URI for the number in the PhoneLookup table, with a parameter to indicate whether
406         // the number is a SIP number.
407         Uri uri = PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI;
408         if (!ContactsUtils.FLAG_N_FEATURE) {
409             if (directoryId != -1) {
410                 // ENTERPRISE_CONTENT_FILTER_URI in M doesn't support directory lookup
411                 uri = PhoneLookup.CONTENT_FILTER_URI;
412             } else {
413                 // b/25900607 in M. PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, encodes twice.
414                 number = Uri.encode(number);
415             }
416         }
417         Uri.Builder builder = uri.buildUpon()
418                 .appendPath(number)
419                 .appendQueryParameter(PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS,
420                 String.valueOf(PhoneNumberHelper.isUriNumber(number)));
421         if (directoryId != -1) {
422             builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
423                     String.valueOf(directoryId));
424         }
425         return builder.build();
426     }
427 
428     /**
429      * Returns the contact information stored in an entry of the call log.
430      *
431      * @param c A cursor pointing to an entry in the call log.
432      */
getContactInfo(Cursor c)433     public static ContactInfo getContactInfo(Cursor c) {
434         ContactInfo info = new ContactInfo();
435         info.lookupUri = UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_LOOKUP_URI));
436         info.name = c.getString(CallLogQuery.CACHED_NAME);
437         info.type = c.getInt(CallLogQuery.CACHED_NUMBER_TYPE);
438         info.label = c.getString(CallLogQuery.CACHED_NUMBER_LABEL);
439         String matchedNumber = c.getString(CallLogQuery.CACHED_MATCHED_NUMBER);
440         String postDialDigits = CompatUtils.isNCompatible()
441                 ? c.getString(CallLogQuery.POST_DIAL_DIGITS) : "";
442         info.number = (matchedNumber == null) ?
443                 c.getString(CallLogQuery.NUMBER) + postDialDigits : matchedNumber;
444 
445         info.normalizedNumber = c.getString(CallLogQuery.CACHED_NORMALIZED_NUMBER);
446         info.photoId = c.getLong(CallLogQuery.CACHED_PHOTO_ID);
447         info.photoUri = DialerCompatUtils.isCallsCachedPhotoUriCompatible() ?
448                 UriUtils.nullForNonContactsUri(
449                         UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_PHOTO_URI)))
450                 : null;
451         info.formattedNumber = c.getString(CallLogQuery.CACHED_FORMATTED_NUMBER);
452 
453         return info;
454     }
455 
456     /**
457      * Given a contact's sourceType, return true if the contact is a business
458      *
459      * @param sourceType sourceType of the contact. This is usually populated by
460      *        {@link #mCachedNumberLookupService}.
461      */
isBusiness(int sourceType)462     public boolean isBusiness(int sourceType) {
463         return mCachedNumberLookupService != null
464                 && mCachedNumberLookupService.isBusiness(sourceType);
465     }
466 
467     /**
468      * This function looks at a contact's source and determines if the user can
469      * mark caller ids from this source as invalid.
470      *
471      * @param sourceType The source type to be checked
472      * @param objectId The ID of the Contact object.
473      * @return true if contacts from this source can be marked with an invalid caller id
474      */
canReportAsInvalid(int sourceType, String objectId)475     public boolean canReportAsInvalid(int sourceType, String objectId) {
476         return mCachedNumberLookupService != null
477                 && mCachedNumberLookupService.canReportAsInvalid(sourceType, objectId);
478     }
479 }
480