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