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.support.annotation.Nullable;
29import android.telephony.PhoneNumberUtils;
30import android.text.TextUtils;
31import android.util.Log;
32
33import com.android.contacts.common.ContactsUtils;
34import com.android.contacts.common.ContactsUtils.UserType;
35import com.android.contacts.common.compat.CompatUtils;
36import com.android.contacts.common.util.Constants;
37import com.android.contacts.common.util.PermissionsUtil;
38import com.android.contacts.common.util.PhoneNumberHelper;
39import com.android.contacts.common.util.UriUtils;
40import com.android.dialer.compat.DialerCompatUtils;
41import com.android.dialer.service.CachedNumberLookupService;
42import com.android.dialer.service.CachedNumberLookupService.CachedContactInfo;
43import com.android.dialer.util.TelecomUtil;
44import com.android.dialerbind.ObjectFactory;
45
46import org.json.JSONException;
47import org.json.JSONObject;
48
49/**
50 * Utility class to look up the contact information for a given number.
51 */
52public 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
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
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     */
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     */
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
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
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     */
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     */
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     */
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
400    public static Uri getContactInfoLookupUri(String number) {
401        return getContactInfoLookupUri(number, -1);
402    }
403
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     */
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     */
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     */
475    public boolean canReportAsInvalid(int sourceType, String objectId) {
476        return mCachedNumberLookupService != null
477                && mCachedNumberLookupService.canReportAsInvalid(sourceType, objectId);
478    }
479}
480