Home | History | Annotate | Download | only in util
      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 
     17 package com.android.messaging.util;
     18 
     19 import android.Manifest;
     20 import android.content.Context;
     21 import android.content.pm.PackageManager;
     22 import android.database.Cursor;
     23 import android.net.Uri;
     24 import android.provider.ContactsContract;
     25 import android.provider.ContactsContract.CommonDataKinds.Email;
     26 import android.provider.ContactsContract.CommonDataKinds.Phone;
     27 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
     28 import android.provider.ContactsContract.Contacts;
     29 import android.provider.ContactsContract.Directory;
     30 import android.provider.ContactsContract.DisplayNameSources;
     31 import android.provider.ContactsContract.PhoneLookup;
     32 import android.provider.ContactsContract.Profile;
     33 import android.text.TextUtils;
     34 import android.view.View;
     35 
     36 import com.android.ex.chips.RecipientEntry;
     37 import com.android.messaging.Factory;
     38 import com.android.messaging.datamodel.CursorQueryData;
     39 import com.android.messaging.datamodel.FrequentContactsCursorQueryData;
     40 import com.android.messaging.datamodel.data.ParticipantData;
     41 import com.android.messaging.sms.MmsSmsUtils;
     42 import com.android.messaging.ui.contact.AddContactsConfirmationDialog;
     43 import 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
     49 public 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