• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
ContactUtil()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      */
showOrAddContact(final View view, final long contactId, final String contactLookupKey, final Uri avatarUri, final String normalizedDestination)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
getSelf(final Context context)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
getPhones(final Context context)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      */
lookupDestination(final Context context, final String destination)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      */
shouldFilterForEmail(final String searchText)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      */
filterDestination(final Context context, final String destination)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
filterPhones(final Context context, final String query)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
lookupPhone(final Context context, final String phone)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      */
getFrequentContacts(final Context context)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
filterEmails(final Context context, final String query)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
lookupEmail(final Context context, final String email)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      */
lookupStructuredName(final Context context, final long contactId, final boolean primaryOnly)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      */
lookupFirstName(final Context context, final long contactId)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      */
createRecipientEntry(final String displayName, final int displayNameSource, final String destination, final int destinationType, final String destinationLabel, final long contactId, final String lookupKey, final long dataId, final String photoThumbnailUri, final boolean firstLevel)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      */
createRecipientEntryForPhoneQuery(final Cursor cursor, final boolean isFirstLevel)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      */
isValidContactId(final long contactId)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      */
isEnterpriseContactId(final long contactId)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      */
isWorkProfileSupported()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      */
getEmailContentLookupUri()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      */
getPhoneLookupUri()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 
hasReadContactsPermission()522     public static boolean hasReadContactsPermission() {
523         return OsUtil.hasPermission(Manifest.permission.READ_CONTACTS);
524     }
525 }
526