1/*
2 * Copyright (C) 2015 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
17package com.android.messaging.util;
18
19import android.Manifest;
20import android.content.Context;
21import android.content.pm.PackageManager;
22import android.database.Cursor;
23import android.net.Uri;
24import android.provider.ContactsContract;
25import android.provider.ContactsContract.CommonDataKinds.Email;
26import android.provider.ContactsContract.CommonDataKinds.Phone;
27import android.provider.ContactsContract.CommonDataKinds.StructuredName;
28import android.provider.ContactsContract.Contacts;
29import android.provider.ContactsContract.Directory;
30import android.provider.ContactsContract.DisplayNameSources;
31import android.provider.ContactsContract.PhoneLookup;
32import android.provider.ContactsContract.Profile;
33import android.text.TextUtils;
34import android.view.View;
35
36import com.android.ex.chips.RecipientEntry;
37import com.android.messaging.Factory;
38import com.android.messaging.datamodel.CursorQueryData;
39import com.android.messaging.datamodel.FrequentContactsCursorQueryData;
40import com.android.messaging.datamodel.data.ParticipantData;
41import com.android.messaging.sms.MmsSmsUtils;
42import com.android.messaging.ui.contact.AddContactsConfirmationDialog;
43import com.google.common.annotations.VisibleForTesting;
44
45/**
46 * Utility class including logic to list, filter, and lookup phone and emails in CP2.
47 */
48@VisibleForTesting
49public class ContactUtil {
50
51    /**
52     * Index of different columns in phone or email queries. All queries below should confirm to
53     * this column content and ordering so that caller can use the uniformed way to process
54     * returned cursors.
55     */
56    public static final int INDEX_CONTACT_ID              = 0;
57    public static final int INDEX_DISPLAY_NAME            = 1;
58    public static final int INDEX_PHOTO_URI               = 2;
59    public static final int INDEX_PHONE_EMAIL             = 3;
60    public static final int INDEX_PHONE_EMAIL_TYPE        = 4;
61    public static final int INDEX_PHONE_EMAIL_LABEL       = 5;
62
63    // An optional lookup_id column used by PhoneLookupQuery that is needed when querying for
64    // contact information.
65    public static final int INDEX_LOOKUP_KEY              = 6;
66
67    // An optional _id column to query results that need to be displayed in a list view.
68    public static final int INDEX_DATA_ID                 = 7;
69
70    // An optional sort_key column for displaying contact section labels.
71    public static final int INDEX_SORT_KEY                = 8;
72
73    // Lookup key column index specific to frequent contacts query.
74    public static final int INDEX_LOOKUP_KEY_FREQUENT     = 3;
75
76    /**
77     * Constants for listing and filtering phones.
78     */
79    public static class PhoneQuery {
80        public static final String SORT_KEY = Phone.SORT_KEY_PRIMARY;
81
82        public static final String[] PROJECTION = new String[] {
83            Phone.CONTACT_ID,                   // 0
84            Phone.DISPLAY_NAME_PRIMARY,         // 1
85            Phone.PHOTO_THUMBNAIL_URI,          // 2
86            Phone.NUMBER,                       // 3
87            Phone.TYPE,                         // 4
88            Phone.LABEL,                        // 5
89            Phone.LOOKUP_KEY,                   // 6
90            Phone._ID,                          // 7
91            PhoneQuery.SORT_KEY,                // 8
92        };
93    }
94
95    /**
96     * Constants for looking up phone numbers.
97     */
98    public static class PhoneLookupQuery {
99        public static final String[] PROJECTION = new String[] {
100            // The _ID field points to the contact id of the content
101            PhoneLookup._ID,                          // 0
102            PhoneLookup.DISPLAY_NAME,                 // 1
103            PhoneLookup.PHOTO_THUMBNAIL_URI,          // 2
104            PhoneLookup.NUMBER,                       // 3
105            PhoneLookup.TYPE,                         // 4
106            PhoneLookup.LABEL,                        // 5
107            PhoneLookup.LOOKUP_KEY,                   // 6
108            // The data id is not included as part of the projection since it's not part of
109            // PhoneLookup. This is okay because the _id field serves as both the data id and
110            // contact id. Also we never show the results directly in a list view so we are not
111            // concerned about duplicated _id's (namely, the same contact has two same phone
112            // numbers)
113        };
114    }
115
116    public static class FrequentContactQuery {
117        public static final String[] PROJECTION = new String[] {
118            Contacts._ID,                       // 0
119            Contacts.DISPLAY_NAME,              // 1
120            Contacts.PHOTO_URI,                 // 2
121            Phone.LOOKUP_KEY,                   // 3
122        };
123    }
124
125    /**
126     * Constants for listing and filtering emails.
127     */
128    public static class EmailQuery {
129        public static final String SORT_KEY = Email.SORT_KEY_PRIMARY;
130
131        public static final String[] PROJECTION = new String[] {
132            Email.CONTACT_ID,                   // 0
133            Email.DISPLAY_NAME_PRIMARY,         // 1
134            Email.PHOTO_THUMBNAIL_URI,          // 2
135            Email.ADDRESS,                      // 3
136            Email.TYPE,                         // 4
137            Email.LABEL,                        // 5
138            Email.LOOKUP_KEY,                   // 6
139            Email._ID,                          // 7
140            EmailQuery.SORT_KEY,                // 8
141        };
142    }
143
144    public static final int INDEX_SELF_QUERY_LOOKUP_KEY = 3;
145
146    /**
147     * Constants for querying self from CP2.
148     */
149    public static class SelfQuery {
150        public static final String[] PROJECTION = new String[] {
151            Profile._ID,                        // 0
152            Profile.DISPLAY_NAME_PRIMARY,       // 1
153            Profile.PHOTO_THUMBNAIL_URI,        // 2
154            Profile.LOOKUP_KEY                  // 3
155            // Phone number, type, label and data_id is not provided in this projection since
156            // Profile CONTENT_URI doesn't include this information. Also, we don't need it
157            // we just need the name and avatar url.
158        };
159    }
160
161    public static class StructuredNameQuery {
162        public static final String[] PROJECTION = new String[] {
163            StructuredName.DISPLAY_NAME,
164            StructuredName.GIVEN_NAME,
165            StructuredName.FAMILY_NAME,
166            StructuredName.PREFIX,
167            StructuredName.MIDDLE_NAME,
168            StructuredName.SUFFIX
169        };
170    }
171
172    public static final int INDEX_STRUCTURED_NAME_DISPLAY_NAME = 0;
173    public static final int INDEX_STRUCTURED_NAME_GIVEN_NAME = 1;
174    public static final int INDEX_STRUCTURED_NAME_FAMILY_NAME = 2;
175    public static final int INDEX_STRUCTURED_NAME_PREFIX = 3;
176    public static final int INDEX_STRUCTURED_NAME_MIDDLE_NAME = 4;
177    public static final int INDEX_STRUCTURED_NAME_SUFFIX = 5;
178
179    public static final long INVALID_CONTACT_ID = -1;
180
181    /**
182     * This class is static. No need to create an instance.
183     */
184    private ContactUtil() {
185    }
186
187    /**
188     * Shows a contact card or add to contacts dialog for the given contact info
189     * @param view The view whose click triggered this to show
190     * @param contactId The id of the contact in the android contacts DB
191     * @param contactLookupKey The lookup key from contacts DB
192     * @param avatarUri Uri to the avatar image if available
193     * @param normalizedDestination The normalized phone number or email
194     */
195    public static void showOrAddContact(final View view, final long contactId,
196            final String contactLookupKey, final Uri avatarUri,
197            final String normalizedDestination) {
198        if (contactId > ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED
199                && !TextUtils.isEmpty(contactLookupKey)) {
200            final Uri lookupUri =
201                    ContactsContract.Contacts.getLookupUri(contactId, contactLookupKey);
202            ContactsContract.QuickContact.showQuickContact(view.getContext(), view, lookupUri,
203                    ContactsContract.QuickContact.MODE_LARGE, null);
204        } else if (!TextUtils.isEmpty(normalizedDestination) && !TextUtils.equals(
205                normalizedDestination, ParticipantData.getUnknownSenderDestination())) {
206            final AddContactsConfirmationDialog dialog = new AddContactsConfirmationDialog(
207                    view.getContext(), avatarUri, normalizedDestination);
208            dialog.show();
209        }
210    }
211
212    @VisibleForTesting
213    public static CursorQueryData getSelf(final Context context) {
214        if (!ContactUtil.hasReadContactsPermission()) {
215            return CursorQueryData.getEmptyQueryData();
216        }
217        return new CursorQueryData(context, Profile.CONTENT_URI, SelfQuery.PROJECTION, null, null,
218                null);
219    }
220
221    /**
222     * Get a list of phones sorted by contact name. One contact may have multiple phones.
223     * In that case, each phone will be returned as a separate record in the result cursor.
224     */
225    @VisibleForTesting
226    public static CursorQueryData getPhones(final Context context) {
227        if (!ContactUtil.hasReadContactsPermission()) {
228            return CursorQueryData.getEmptyQueryData();
229        }
230
231        // The AOSP Contacts provider allows adding a ContactsContract.REMOVE_DUPLICATE_ENTRIES
232        // query parameter that removes duplicate (raw) numbers. Unfortunately, we can't use that
233        // because it causes the some phones' contacts provider to return incorrect sections.
234        final Uri uri = Phone.CONTENT_URI.buildUpon().appendQueryParameter(
235                ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
236                .appendQueryParameter(Contacts.EXTRA_ADDRESS_BOOK_INDEX, "true")
237                .build();
238
239        return new CursorQueryData(context, uri, PhoneQuery.PROJECTION, null, null,
240                PhoneQuery.SORT_KEY);
241    }
242
243    /**
244     * Lookup a destination (phone, email). Supplied destination should be a relatively complete
245     * one for this to succeed. PhoneLookup / EmailLookup URI will apply some smartness to do a
246     * loose match to see whether there is a contact that matches this destination.
247     */
248    public static CursorQueryData lookupDestination(final Context context,
249            final String destination) {
250        if (MmsSmsUtils.isEmailAddress(destination)) {
251            return ContactUtil.lookupEmail(context, destination);
252        } else {
253            return ContactUtil.lookupPhone(context, destination);
254        }
255    }
256
257    /**
258     * Returns whether the search text indicates an email based search or a phone number based one.
259     */
260    private static boolean shouldFilterForEmail(final String searchText) {
261        return searchText != null && searchText.contains("@");
262    }
263
264    /**
265     * Get a list of destinations (phone, email) matching the partial destination.
266     */
267    public static CursorQueryData filterDestination(final Context context,
268            final String destination) {
269        if (shouldFilterForEmail(destination)) {
270            return ContactUtil.filterEmails(context, destination);
271        } else {
272            return ContactUtil.filterPhones(context, destination);
273        }
274    }
275
276    /**
277     * Get a list of destinations (phone, email) matching the partial destination in work profile.
278     */
279    public static CursorQueryData filterDestinationEnterprise(final Context context,
280            final String destination) {
281        if (shouldFilterForEmail(destination)) {
282            return ContactUtil.filterEmailsEnterprise(context, destination);
283        } else {
284            return ContactUtil.filterPhonesEnterprise(context, destination);
285        }
286    }
287
288    /**
289     * Get a list of phones matching a search criteria. The search may be on contact name or
290     * phone number. In case search is on contact name, all matching contact's phone number
291     * will be returned.
292     * NOTE: This is visible for testing only, clients should only call filterDestination() since
293     * we support email addresses as well.
294     */
295    @VisibleForTesting
296    public static CursorQueryData filterPhones(final Context context, final String query) {
297        return filterPhonesInternal(context, Phone.CONTENT_FILTER_URI, query, Directory.DEFAULT);
298    }
299
300    /**
301     * Similar to {@link #filterPhones(Context, String)}, but search in work profile instead.
302     */
303    public static CursorQueryData filterPhonesEnterprise(final Context context,
304            final String query) {
305        return filterPhonesInternal(context, Phone.ENTERPRISE_CONTENT_FILTER_URI, query,
306                Directory.ENTERPRISE_DEFAULT);
307    }
308
309    private static CursorQueryData filterPhonesInternal(final Context context,
310            final Uri phoneFilterBaseUri, final String query, final long directoryId) {
311        if (!ContactUtil.hasReadContactsPermission()) {
312            return CursorQueryData.getEmptyQueryData();
313        }
314        Uri phoneFilterUri = buildDirectorySearchUri(phoneFilterBaseUri, query, directoryId);
315        return new CursorQueryData(context,
316                phoneFilterUri,
317                PhoneQuery.PROJECTION, null, null,
318                PhoneQuery.SORT_KEY);
319    }
320    /**
321     * Lookup a phone based on a phone number. Supplied phone should be a relatively complete
322     * phone number for this to succeed. PhoneLookup URI will apply some smartness to do a
323     * loose match to see whether there is a contact that matches this phone.
324     * NOTE: This is visible for testing only, clients should only call lookupDestination() since
325     * we support email addresses as well.
326     */
327    @VisibleForTesting
328    public static CursorQueryData lookupPhone(final Context context, final String phone) {
329        if (!ContactUtil.hasReadContactsPermission()) {
330            return CursorQueryData.getEmptyQueryData();
331        }
332
333        final Uri uri = getPhoneLookupUri().buildUpon()
334                .appendPath(phone).build();
335
336        return new CursorQueryData(context, uri, PhoneLookupQuery.PROJECTION, null, null, null);
337    }
338
339    /**
340     * Get frequently contacted people. This queries for Contacts.CONTENT_STREQUENT_URI, which
341     * includes both starred or frequently contacted people.
342     */
343    public static CursorQueryData getFrequentContacts(final Context context) {
344        if (!ContactUtil.hasReadContactsPermission()) {
345            return CursorQueryData.getEmptyQueryData();
346        }
347
348        return new FrequentContactsCursorQueryData(context, FrequentContactQuery.PROJECTION,
349                null, null, null);
350    }
351
352    /**
353     * Get a list of emails matching a search criteria. In Bugle, since email is not a common
354     * usage scenario, we should only do email search after user typed in a query indicating
355     * an intention to search by email (for example, "joe@").
356     * NOTE: This is visible for testing only, clients should only call filterDestination() since
357     * we support email addresses as well.
358     */
359    @VisibleForTesting
360    public static CursorQueryData filterEmails(final Context context, final String query) {
361        return filterEmailsInternal(context, Email.CONTENT_FILTER_URI, query, Directory.DEFAULT);
362    }
363
364    /**
365     * Similar to {@link #filterEmails(Context, String)}, but search in work profile instead.
366     */
367    public static CursorQueryData filterEmailsEnterprise(final Context context,
368            final String query) {
369        return filterEmailsInternal(context, Email.ENTERPRISE_CONTENT_FILTER_URI, query,
370                Directory.ENTERPRISE_DEFAULT);
371    }
372
373    private static CursorQueryData filterEmailsInternal(final Context context,
374            final Uri filterEmailsBaseUri, final String query, final long directoryId) {
375        if (!ContactUtil.hasReadContactsPermission()) {
376            return CursorQueryData.getEmptyQueryData();
377        }
378        final Uri filterEmailsUri = buildDirectorySearchUri(filterEmailsBaseUri, query,
379                directoryId);
380        return new CursorQueryData(context,
381                filterEmailsUri,
382                PhoneQuery.PROJECTION, null, null,
383                PhoneQuery.SORT_KEY);
384    }
385
386    /**
387     * Lookup emails based a complete email address. Since there is no special logic needed for
388     * email lookup, this simply calls filterEmails.
389     * NOTE: This is visible for testing only, clients should only call lookupDestination() since
390     * we support email addresses as well.
391     */
392    @VisibleForTesting
393    public static CursorQueryData lookupEmail(final Context context, final String email) {
394        if (!ContactUtil.hasReadContactsPermission()) {
395            return CursorQueryData.getEmptyQueryData();
396        }
397
398        final Uri uri = getEmailContentLookupUri().buildUpon()
399                .appendPath(email).appendQueryParameter(
400                        ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
401                        .build();
402
403        return new CursorQueryData(context, uri, EmailQuery.PROJECTION, null, null,
404                EmailQuery.SORT_KEY);
405    }
406
407    /**
408     * Looks up the structured name for a contact.
409     *
410     * @param primaryOnly If there are multiple raw contacts, set this flag to return only the
411     * name used as the primary display name. Otherwise, this method returns all names.
412     */
413    private static CursorQueryData lookupStructuredName(final Context context, final long contactId,
414            final boolean primaryOnly) {
415        if (!ContactUtil.hasReadContactsPermission()) {
416            return CursorQueryData.getEmptyQueryData();
417        }
418
419        // TODO: Handle enterprise contacts
420        final Uri uri = ContactsContract.Contacts.CONTENT_URI.buildUpon()
421                .appendPath(String.valueOf(contactId))
422                .appendPath(ContactsContract.Contacts.Data.CONTENT_DIRECTORY).build();
423
424        String selection = ContactsContract.Data.MIMETYPE + "=?";
425        final String[] selectionArgs = {
426                StructuredName.CONTENT_ITEM_TYPE
427        };
428        if (primaryOnly) {
429            selection += " AND " + Contacts.DISPLAY_NAME_PRIMARY + "="
430                    + StructuredName.DISPLAY_NAME;
431        }
432
433        return new CursorQueryData(context, uri,
434                StructuredNameQuery.PROJECTION, selection, selectionArgs, null);
435    }
436
437    /**
438     * Looks up the first name for a contact. If there are multiple raw
439     * contacts, this returns the name that is associated with the contact's
440     * primary display name. The name is null when contact id does not exist
441     * (possibly because it is a corp contact) or it does not have a first name.
442     */
443    public static String lookupFirstName(final Context context, final long contactId) {
444        if (isEnterpriseContactId(contactId)) {
445            return null;
446        }
447        String firstName = null;
448        Cursor nameCursor = null;
449        try {
450            nameCursor = ContactUtil.lookupStructuredName(context, contactId, true)
451                    .performSynchronousQuery();
452            if (nameCursor != null && nameCursor.moveToFirst()) {
453                firstName = nameCursor.getString(ContactUtil.INDEX_STRUCTURED_NAME_GIVEN_NAME);
454            }
455        } finally {
456            if (nameCursor != null) {
457                nameCursor.close();
458            }
459        }
460        return firstName;
461    }
462
463    /**
464     * Creates a RecipientEntry from the provided data fields (from the contacts cursor).
465     * @param firstLevel whether this item is the first entry of this contact in the list.
466     */
467    public static RecipientEntry createRecipientEntry(final String displayName,
468            final int displayNameSource, final String destination, final int destinationType,
469            final String destinationLabel, final long contactId, final String lookupKey,
470            final long dataId, final String photoThumbnailUri, final boolean firstLevel) {
471        if (firstLevel) {
472            return RecipientEntry.constructTopLevelEntry(displayName, displayNameSource,
473                    destination, destinationType, destinationLabel, contactId, null, dataId,
474                    photoThumbnailUri, true, lookupKey);
475        } else {
476            return RecipientEntry.constructSecondLevelEntry(displayName, displayNameSource,
477                    destination, destinationType, destinationLabel, contactId, null, dataId,
478                    photoThumbnailUri, true, lookupKey);
479        }
480    }
481
482    /**
483     * Creates a RecipientEntry for PhoneQuery result. The result is then displayed in the
484     * contact search drop down or as replacement chips in the chips edit box.
485     */
486    public static RecipientEntry createRecipientEntryForPhoneQuery(final Cursor cursor,
487            final boolean isFirstLevel) {
488        final long contactId = cursor.getLong(ContactUtil.INDEX_CONTACT_ID);
489        final String displayName = cursor.getString(
490                ContactUtil.INDEX_DISPLAY_NAME);
491        final String photoThumbnailUri = cursor.getString(
492                ContactUtil.INDEX_PHOTO_URI);
493        final String destination = cursor.getString(
494                ContactUtil.INDEX_PHONE_EMAIL);
495        final int destinationType = cursor.getInt(
496                ContactUtil.INDEX_PHONE_EMAIL_TYPE);
497        final String destinationLabel = cursor.getString(
498                ContactUtil.INDEX_PHONE_EMAIL_LABEL);
499        final String lookupKey = cursor.getString(
500                ContactUtil.INDEX_LOOKUP_KEY);
501
502        // PhoneQuery uses the contact id as the data id ("_id").
503        final long dataId = contactId;
504
505        return createRecipientEntry(displayName,
506                DisplayNameSources.STRUCTURED_NAME, destination, destinationType,
507                destinationLabel, contactId, lookupKey, dataId, photoThumbnailUri,
508                isFirstLevel);
509    }
510
511    /**
512     * Returns if a given contact id is valid.
513     */
514    public static boolean isValidContactId(final long contactId) {
515        return contactId >= 0;
516    }
517
518    /**
519     * Returns if a given contact id belongs to managed profile.
520     */
521    public static boolean isEnterpriseContactId(final long contactId) {
522        return OsUtil.isAtLeastL() && ContactsContract.Contacts.isEnterpriseContactId(contactId);
523    }
524
525    /**
526     * Returns Email lookup uri that will query both primary and corp profile
527     */
528    private static Uri getEmailContentLookupUri() {
529        if (OsUtil.isAtLeastM()) {
530            return Email.ENTERPRISE_CONTENT_LOOKUP_URI;
531        }
532        return Email.CONTENT_LOOKUP_URI;
533    }
534
535    /**
536     * Returns PhoneLookup URI.
537     */
538    public static Uri getPhoneLookupUri() {
539        if (OsUtil.isAtLeastM()) {
540            return PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI;
541        }
542        return PhoneLookup.CONTENT_FILTER_URI;
543    }
544
545    public static boolean hasReadContactsPermission() {
546        return OsUtil.hasPermission(Manifest.permission.READ_CONTACTS);
547    }
548
549    private static Uri buildDirectorySearchUri(final Uri uri, final String query,
550            final long directoryId) {
551        return uri.buildUpon()
552                .appendPath(query).appendQueryParameter(
553                        ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId))
554                .build();
555    }
556}
557