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 phones matching a search criteria. The search may be on contact name or
    278      * phone number. In case search is on contact name, all matching contact's phone number
    279      * will be returned.
    280      * NOTE: This is visible for testing only, clients should only call filterDestination() since
    281      * we support email addresses as well.
    282      */
    283     @VisibleForTesting
    284     public static CursorQueryData filterPhones(final Context context, final String query) {
    285         if (!ContactUtil.hasReadContactsPermission()) {
    286             return CursorQueryData.getEmptyQueryData();
    287         }
    288 
    289         final Uri uri = Phone.CONTENT_FILTER_URI.buildUpon()
    290                 .appendPath(query).appendQueryParameter(
    291                         ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
    292                         .build();
    293 
    294         return new CursorQueryData(context, uri, PhoneQuery.PROJECTION, null, null,
    295                 PhoneQuery.SORT_KEY);
    296     }
    297 
    298     /**
    299      * Lookup a phone based on a phone number. Supplied phone should be a relatively complete
    300      * phone number for this to succeed. PhoneLookup URI will apply some smartness to do a
    301      * loose match to see whether there is a contact that matches this phone.
    302      * NOTE: This is visible for testing only, clients should only call lookupDestination() since
    303      * we support email addresses as well.
    304      */
    305     @VisibleForTesting
    306     public static CursorQueryData lookupPhone(final Context context, final String phone) {
    307         if (!ContactUtil.hasReadContactsPermission()) {
    308             return CursorQueryData.getEmptyQueryData();
    309         }
    310 
    311         final Uri uri = getPhoneLookupUri().buildUpon()
    312                 .appendPath(phone).build();
    313 
    314         return new CursorQueryData(context, uri, PhoneLookupQuery.PROJECTION, null, null, null);
    315     }
    316 
    317     /**
    318      * Get frequently contacted people. This queries for Contacts.CONTENT_STREQUENT_URI, which
    319      * includes both starred or frequently contacted people.
    320      */
    321     public static CursorQueryData getFrequentContacts(final Context context) {
    322         if (!ContactUtil.hasReadContactsPermission()) {
    323             return CursorQueryData.getEmptyQueryData();
    324         }
    325 
    326         return new FrequentContactsCursorQueryData(context, FrequentContactQuery.PROJECTION,
    327                 null, null, null);
    328     }
    329 
    330     /**
    331      * Get a list of emails matching a search criteria. In Bugle, since email is not a common
    332      * usage scenario, we should only do email search after user typed in a query indicating
    333      * an intention to search by email (for example, "joe@").
    334      * NOTE: This is visible for testing only, clients should only call filterDestination() since
    335      * we support email addresses as well.
    336      */
    337     @VisibleForTesting
    338     public static CursorQueryData filterEmails(final Context context, final String query) {
    339         if (!ContactUtil.hasReadContactsPermission()) {
    340             return CursorQueryData.getEmptyQueryData();
    341         }
    342 
    343         final Uri uri = Email.CONTENT_FILTER_URI.buildUpon()
    344                 .appendPath(query).appendQueryParameter(
    345                         ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
    346                         .build();
    347 
    348         return new CursorQueryData(context, uri, EmailQuery.PROJECTION, null, null,
    349                 EmailQuery.SORT_KEY);
    350     }
    351 
    352     /**
    353      * Lookup emails based a complete email address. Since there is no special logic needed for
    354      * email lookup, this simply calls filterEmails.
    355      * NOTE: This is visible for testing only, clients should only call lookupDestination() since
    356      * we support email addresses as well.
    357      */
    358     @VisibleForTesting
    359     public static CursorQueryData lookupEmail(final Context context, final String email) {
    360         if (!ContactUtil.hasReadContactsPermission()) {
    361             return CursorQueryData.getEmptyQueryData();
    362         }
    363 
    364         final Uri uri = getEmailContentLookupUri().buildUpon()
    365                 .appendPath(email).appendQueryParameter(
    366                         ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
    367                         .build();
    368 
    369         return new CursorQueryData(context, uri, EmailQuery.PROJECTION, null, null,
    370                 EmailQuery.SORT_KEY);
    371     }
    372 
    373     /**
    374      * Looks up the structured name for a contact.
    375      *
    376      * @param primaryOnly If there are multiple raw contacts, set this flag to return only the
    377      * name used as the primary display name. Otherwise, this method returns all names.
    378      */
    379     private static CursorQueryData lookupStructuredName(final Context context, final long contactId,
    380             final boolean primaryOnly) {
    381         if (!ContactUtil.hasReadContactsPermission()) {
    382             return CursorQueryData.getEmptyQueryData();
    383         }
    384 
    385         // TODO: Handle enterprise contacts
    386         final Uri uri = ContactsContract.Contacts.CONTENT_URI.buildUpon()
    387                 .appendPath(String.valueOf(contactId))
    388                 .appendPath(ContactsContract.Contacts.Data.CONTENT_DIRECTORY).build();
    389 
    390         String selection = ContactsContract.Data.MIMETYPE + "=?";
    391         final String[] selectionArgs = {
    392                 StructuredName.CONTENT_ITEM_TYPE
    393         };
    394         if (primaryOnly) {
    395             selection += " AND " + Contacts.DISPLAY_NAME_PRIMARY + "="
    396                     + StructuredName.DISPLAY_NAME;
    397         }
    398 
    399         return new CursorQueryData(context, uri,
    400                 StructuredNameQuery.PROJECTION, selection, selectionArgs, null);
    401     }
    402 
    403     /**
    404      * Looks up the first name for a contact. If there are multiple raw
    405      * contacts, this returns the name that is associated with the contact's
    406      * primary display name. The name is null when contact id does not exist
    407      * (possibly because it is a corp contact) or it does not have a first name.
    408      */
    409     public static String lookupFirstName(final Context context, final long contactId) {
    410         if (isEnterpriseContactId(contactId)) {
    411             return null;
    412         }
    413         String firstName = null;
    414         Cursor nameCursor = null;
    415         try {
    416             nameCursor = ContactUtil.lookupStructuredName(context, contactId, true)
    417                     .performSynchronousQuery();
    418             if (nameCursor != null && nameCursor.moveToFirst()) {
    419                 firstName = nameCursor.getString(ContactUtil.INDEX_STRUCTURED_NAME_GIVEN_NAME);
    420             }
    421         } finally {
    422             if (nameCursor != null) {
    423                 nameCursor.close();
    424             }
    425         }
    426         return firstName;
    427     }
    428 
    429     /**
    430      * Creates a RecipientEntry from the provided data fields (from the contacts cursor).
    431      * @param firstLevel whether this item is the first entry of this contact in the list.
    432      */
    433     public static RecipientEntry createRecipientEntry(final String displayName,
    434             final int displayNameSource, final String destination, final int destinationType,
    435             final String destinationLabel, final long contactId, final String lookupKey,
    436             final long dataId, final String photoThumbnailUri, final boolean firstLevel) {
    437         if (firstLevel) {
    438             return RecipientEntry.constructTopLevelEntry(displayName, displayNameSource,
    439                     destination, destinationType, destinationLabel, contactId, null, dataId,
    440                     photoThumbnailUri, true, lookupKey);
    441         } else {
    442             return RecipientEntry.constructSecondLevelEntry(displayName, displayNameSource,
    443                     destination, destinationType, destinationLabel, contactId, null, dataId,
    444                     photoThumbnailUri, true, lookupKey);
    445         }
    446     }
    447 
    448     /**
    449      * Creates a RecipientEntry for PhoneQuery result. The result is then displayed in the
    450      * contact search drop down or as replacement chips in the chips edit box.
    451      */
    452     public static RecipientEntry createRecipientEntryForPhoneQuery(final Cursor cursor,
    453             final boolean isFirstLevel) {
    454         final long contactId = cursor.getLong(ContactUtil.INDEX_CONTACT_ID);
    455         final String displayName = cursor.getString(
    456                 ContactUtil.INDEX_DISPLAY_NAME);
    457         final String photoThumbnailUri = cursor.getString(
    458                 ContactUtil.INDEX_PHOTO_URI);
    459         final String destination = cursor.getString(
    460                 ContactUtil.INDEX_PHONE_EMAIL);
    461         final int destinationType = cursor.getInt(
    462                 ContactUtil.INDEX_PHONE_EMAIL_TYPE);
    463         final String destinationLabel = cursor.getString(
    464                 ContactUtil.INDEX_PHONE_EMAIL_LABEL);
    465         final String lookupKey = cursor.getString(
    466                 ContactUtil.INDEX_LOOKUP_KEY);
    467 
    468         // PhoneQuery uses the contact id as the data id ("_id").
    469         final long dataId = contactId;
    470 
    471         return createRecipientEntry(displayName,
    472                 DisplayNameSources.STRUCTURED_NAME, destination, destinationType,
    473                 destinationLabel, contactId, lookupKey, dataId, photoThumbnailUri,
    474                 isFirstLevel);
    475     }
    476 
    477     /**
    478      * Returns if a given contact id is valid.
    479      */
    480     public static boolean isValidContactId(final long contactId) {
    481         return contactId >= 0;
    482     }
    483 
    484     /**
    485      * Returns if a given contact id belongs to managed profile.
    486      */
    487     public static boolean isEnterpriseContactId(final long contactId) {
    488         return isWorkProfileSupported()
    489                 && ContactsContract.Contacts.isEnterpriseContactId(contactId);
    490     }
    491 
    492     /**
    493      * Returns if managed profile is supported.
    494      */
    495     public static boolean isWorkProfileSupported() {
    496         final PackageManager pm = Factory.get().getApplicationContext().getPackageManager();
    497         return pm.hasSystemFeature(PackageManager.FEATURE_MANAGED_USERS);
    498     }
    499 
    500     /**
    501      * Returns Email lookup uri that will query both primary and corp profile
    502      */
    503     private static Uri getEmailContentLookupUri() {
    504         if (isWorkProfileSupported() && OsUtil.isAtLeastM()) {
    505             // TODO: use Email.ENTERPRISE_CONTENT_LOOKUP_URI, which will be available in M SDK API
    506             return Uri.parse("content://com.android.contacts/data/emails/lookup_enterprise");
    507         }
    508         return Email.CONTENT_LOOKUP_URI;
    509     }
    510 
    511     /**
    512      * Returns PhoneLookup URI.
    513      */
    514     public static Uri getPhoneLookupUri() {
    515         // Apply it to M only
    516         if (isWorkProfileSupported() && OsUtil.isAtLeastM()) {
    517             return PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI;
    518         }
    519         return PhoneLookup.CONTENT_FILTER_URI;
    520     }
    521 
    522     public static boolean hasReadContactsPermission() {
    523         return OsUtil.hasPermission(Manifest.permission.READ_CONTACTS);
    524     }
    525 }
    526