1 /*
2  * Copyright (C) 2010 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.contacts.model;
18 
19 import android.content.AsyncTaskLoader;
20 import android.content.ContentResolver;
21 import android.content.ContentUris;
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.pm.PackageManager;
26 import android.content.pm.PackageManager.NameNotFoundException;
27 import android.content.pm.ResolveInfo;
28 import android.content.res.AssetFileDescriptor;
29 import android.content.res.Resources;
30 import android.database.Cursor;
31 import android.net.Uri;
32 import android.provider.ContactsContract;
33 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
34 import android.provider.ContactsContract.Contacts;
35 import android.provider.ContactsContract.Data;
36 import android.provider.ContactsContract.Directory;
37 import android.provider.ContactsContract.Groups;
38 import android.provider.ContactsContract.RawContacts;
39 import android.text.TextUtils;
40 import android.util.Log;
41 import com.android.contacts.GeoUtil;
42 import com.android.contacts.GroupMetaDataLoader;
43 import com.android.contacts.compat.CompatUtils;
44 import com.android.contacts.group.GroupMetaData;
45 import com.android.contacts.model.account.AccountType;
46 import com.android.contacts.model.account.GoogleAccountType;
47 import com.android.contacts.model.dataitem.DataItem;
48 import com.android.contacts.model.dataitem.PhoneDataItem;
49 import com.android.contacts.model.dataitem.PhotoDataItem;
50 import com.android.contacts.util.Constants;
51 import com.android.contacts.util.ContactLoaderUtils;
52 import com.android.contacts.util.DataStatus;
53 import com.android.contacts.util.UriUtils;
54 import com.google.common.collect.ImmutableList;
55 import com.google.common.collect.ImmutableMap;
56 import com.google.common.collect.Lists;
57 import com.google.common.collect.Sets;
58 import java.io.ByteArrayOutputStream;
59 import java.io.IOException;
60 import java.io.InputStream;
61 import java.net.URL;
62 import java.util.ArrayList;
63 import java.util.HashSet;
64 import java.util.Iterator;
65 import java.util.List;
66 import java.util.Objects;
67 import java.util.Set;
68 import org.json.JSONArray;
69 import org.json.JSONException;
70 import org.json.JSONObject;
71 
72 /**
73  * Loads a single Contact and all it constituent RawContacts.
74  */
75 public class ContactLoader extends AsyncTaskLoader<Contact> {
76 
77     private static final String TAG = ContactLoader.class.getSimpleName();
78 
79     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
80 
81     /** A short-lived cache that can be set by {@link #cacheResult()} */
82     private static Contact sCachedResult = null;
83 
84     private final Uri mRequestedUri;
85     private Uri mLookupUri;
86     private boolean mLoadGroupMetaData;
87     private boolean mPostViewNotification;
88     private boolean mComputeFormattedPhoneNumber;
89     private Contact mContact;
90     private ForceLoadContentObserver mObserver;
91     private final Set<Long> mNotifiedRawContactIds = Sets.newHashSet();
92 
ContactLoader(Context context, Uri lookupUri, boolean postViewNotification)93     public ContactLoader(Context context, Uri lookupUri, boolean postViewNotification) {
94         this(context, lookupUri, false, postViewNotification, false);
95     }
96 
ContactLoader(Context context, Uri lookupUri, boolean postViewNotification, boolean loadGroupMetaData)97     public ContactLoader(Context context, Uri lookupUri, boolean postViewNotification,
98             boolean loadGroupMetaData) {
99         this(context, lookupUri, loadGroupMetaData, postViewNotification, false);
100     }
101 
ContactLoader(Context context, Uri lookupUri, boolean loadGroupMetaData, boolean postViewNotification, boolean computeFormattedPhoneNumber)102     public ContactLoader(Context context, Uri lookupUri, boolean loadGroupMetaData,
103             boolean postViewNotification, boolean computeFormattedPhoneNumber) {
104         super(context);
105         mLookupUri = lookupUri;
106         mRequestedUri = lookupUri;
107         mLoadGroupMetaData = loadGroupMetaData;
108         mPostViewNotification = postViewNotification;
109         mComputeFormattedPhoneNumber = computeFormattedPhoneNumber;
110     }
111 
112     /**
113      * Projection used for the query that loads all data for the entire contact (except for
114      * social stream items).
115      */
116     private static class ContactQuery {
117         static final String[] COLUMNS_INTERNAL = new String[] {
118                 Contacts.NAME_RAW_CONTACT_ID,
119                 Contacts.DISPLAY_NAME_SOURCE,
120                 Contacts.LOOKUP_KEY,
121                 Contacts.DISPLAY_NAME,
122                 Contacts.DISPLAY_NAME_ALTERNATIVE,
123                 Contacts.PHONETIC_NAME,
124                 Contacts.PHOTO_ID,
125                 Contacts.STARRED,
126                 Contacts.CONTACT_PRESENCE,
127                 Contacts.CONTACT_STATUS,
128                 Contacts.CONTACT_STATUS_TIMESTAMP,
129                 Contacts.CONTACT_STATUS_RES_PACKAGE,
130                 Contacts.CONTACT_STATUS_LABEL,
131                 Contacts.Entity.CONTACT_ID,
132                 Contacts.Entity.RAW_CONTACT_ID,
133 
134                 RawContacts.ACCOUNT_NAME,
135                 RawContacts.ACCOUNT_TYPE,
136                 RawContacts.DATA_SET,
137                 RawContacts.DIRTY,
138                 RawContacts.VERSION,
139                 RawContacts.SOURCE_ID,
140                 RawContacts.SYNC1,
141                 RawContacts.SYNC2,
142                 RawContacts.SYNC3,
143                 RawContacts.SYNC4,
144                 RawContacts.DELETED,
145 
146                 Contacts.Entity.DATA_ID,
147                 Data.DATA1,
148                 Data.DATA2,
149                 Data.DATA3,
150                 Data.DATA4,
151                 Data.DATA5,
152                 Data.DATA6,
153                 Data.DATA7,
154                 Data.DATA8,
155                 Data.DATA9,
156                 Data.DATA10,
157                 Data.DATA11,
158                 Data.DATA12,
159                 Data.DATA13,
160                 Data.DATA14,
161                 Data.DATA15,
162                 Data.SYNC1,
163                 Data.SYNC2,
164                 Data.SYNC3,
165                 Data.SYNC4,
166                 Data.DATA_VERSION,
167                 Data.IS_PRIMARY,
168                 Data.IS_SUPER_PRIMARY,
169                 Data.MIMETYPE,
170 
171                 GroupMembership.GROUP_SOURCE_ID,
172 
173                 Data.PRESENCE,
174                 Data.CHAT_CAPABILITY,
175                 Data.STATUS,
176                 Data.STATUS_RES_PACKAGE,
177                 Data.STATUS_ICON,
178                 Data.STATUS_LABEL,
179                 Data.STATUS_TIMESTAMP,
180 
181                 Contacts.PHOTO_URI,
182                 Contacts.SEND_TO_VOICEMAIL,
183                 Contacts.CUSTOM_RINGTONE,
184                 Contacts.IS_USER_PROFILE,
185         };
186 
187         static final String[] COLUMNS;
188 
189         static {
190             List<String> projectionList = Lists.newArrayList(COLUMNS_INTERNAL);
191             if (CompatUtils.isMarshmallowCompatible()) {
192                 projectionList.add(Data.CARRIER_PRESENCE);
193             }
194             COLUMNS = projectionList.toArray(new String[projectionList.size()]);
195         }
196 
197         public static final int NAME_RAW_CONTACT_ID = 0;
198         public static final int DISPLAY_NAME_SOURCE = 1;
199         public static final int LOOKUP_KEY = 2;
200         public static final int DISPLAY_NAME = 3;
201         public static final int ALT_DISPLAY_NAME = 4;
202         public static final int PHONETIC_NAME = 5;
203         public static final int PHOTO_ID = 6;
204         public static final int STARRED = 7;
205         public static final int CONTACT_PRESENCE = 8;
206         public static final int CONTACT_STATUS = 9;
207         public static final int CONTACT_STATUS_TIMESTAMP = 10;
208         public static final int CONTACT_STATUS_RES_PACKAGE = 11;
209         public static final int CONTACT_STATUS_LABEL = 12;
210         public static final int CONTACT_ID = 13;
211         public static final int RAW_CONTACT_ID = 14;
212 
213         public static final int ACCOUNT_NAME = 15;
214         public static final int ACCOUNT_TYPE = 16;
215         public static final int DATA_SET = 17;
216         public static final int DIRTY = 18;
217         public static final int VERSION = 19;
218         public static final int SOURCE_ID = 20;
219         public static final int SYNC1 = 21;
220         public static final int SYNC2 = 22;
221         public static final int SYNC3 = 23;
222         public static final int SYNC4 = 24;
223         public static final int DELETED = 25;
224 
225         public static final int DATA_ID = 26;
226         public static final int DATA1 = 27;
227         public static final int DATA2 = 28;
228         public static final int DATA3 = 29;
229         public static final int DATA4 = 30;
230         public static final int DATA5 = 31;
231         public static final int DATA6 = 32;
232         public static final int DATA7 = 33;
233         public static final int DATA8 = 34;
234         public static final int DATA9 = 35;
235         public static final int DATA10 = 36;
236         public static final int DATA11 = 37;
237         public static final int DATA12 = 38;
238         public static final int DATA13 = 39;
239         public static final int DATA14 = 40;
240         public static final int DATA15 = 41;
241         public static final int DATA_SYNC1 = 42;
242         public static final int DATA_SYNC2 = 43;
243         public static final int DATA_SYNC3 = 44;
244         public static final int DATA_SYNC4 = 45;
245         public static final int DATA_VERSION = 46;
246         public static final int IS_PRIMARY = 47;
247         public static final int IS_SUPERPRIMARY = 48;
248         public static final int MIMETYPE = 49;
249 
250         public static final int GROUP_SOURCE_ID = 50;
251 
252         public static final int PRESENCE = 51;
253         public static final int CHAT_CAPABILITY = 52;
254         public static final int STATUS = 53;
255         public static final int STATUS_RES_PACKAGE = 54;
256         public static final int STATUS_ICON = 55;
257         public static final int STATUS_LABEL = 56;
258         public static final int STATUS_TIMESTAMP = 57;
259 
260         public static final int PHOTO_URI = 58;
261         public static final int SEND_TO_VOICEMAIL = 59;
262         public static final int CUSTOM_RINGTONE = 60;
263         public static final int IS_USER_PROFILE = 61;
264 
265         public static final int CARRIER_PRESENCE = 62;
266     }
267 
268     /**
269      * Projection used for the query that loads all data for the entire contact.
270      */
271     private static class DirectoryQuery {
272         static final String[] COLUMNS = new String[] {
273             Directory.DISPLAY_NAME,
274             Directory.PACKAGE_NAME,
275             Directory.TYPE_RESOURCE_ID,
276             Directory.ACCOUNT_TYPE,
277             Directory.ACCOUNT_NAME,
278             Directory.EXPORT_SUPPORT,
279         };
280 
281         public static final int DISPLAY_NAME = 0;
282         public static final int PACKAGE_NAME = 1;
283         public static final int TYPE_RESOURCE_ID = 2;
284         public static final int ACCOUNT_TYPE = 3;
285         public static final int ACCOUNT_NAME = 4;
286         public static final int EXPORT_SUPPORT = 5;
287     }
288 
setNewLookup(Uri lookupUri)289     public void setNewLookup(Uri lookupUri) {
290         mLookupUri = lookupUri;
291         mContact = null;
292     }
293 
294     @Override
loadInBackground()295     public Contact loadInBackground() {
296         try {
297             final ContentResolver resolver = getContext().getContentResolver();
298             final Uri uriCurrentFormat = ContactLoaderUtils.ensureIsContactUri(
299                     resolver, mLookupUri);
300             final Contact cachedResult = sCachedResult;
301             sCachedResult = null;
302             // Is this the same Uri as what we had before already? In that case, reuse that result
303             final Contact result;
304             final boolean resultIsCached;
305             if (cachedResult != null &&
306                     UriUtils.areEqual(cachedResult.getLookupUri(), mLookupUri)) {
307                 // We are using a cached result from earlier. Below, we should make sure
308                 // we are not doing any more network or disc accesses
309                 result = new Contact(mRequestedUri, cachedResult);
310                 resultIsCached = true;
311             } else {
312                 if (uriCurrentFormat.getLastPathSegment().equals(Constants.LOOKUP_URI_ENCODED)) {
313                     result = loadEncodedContactEntity(uriCurrentFormat, mLookupUri);
314                 } else {
315                     result = loadContactEntity(resolver, uriCurrentFormat);
316                 }
317                 resultIsCached = false;
318             }
319             if (result.isLoaded()) {
320                 if (result.isDirectoryEntry()) {
321                     if (!resultIsCached) {
322                         loadDirectoryMetaData(result);
323                     }
324                 } else if (mLoadGroupMetaData) {
325                     if (result.getGroupMetaData() == null) {
326                         loadGroupMetaData(result);
327                     }
328                 }
329                 if (mComputeFormattedPhoneNumber) {
330                     computeFormattedPhoneNumbers(result);
331                 }
332                 if (!resultIsCached) loadPhotoBinaryData(result);
333 
334             }
335             return result;
336         } catch (Exception e) {
337             Log.e(TAG, "Error loading the contact: " + mLookupUri, e);
338             return Contact.forError(mRequestedUri, e);
339         }
340     }
341 
342     /**
343      * Parses a {@link Contact} stored as a JSON string in a lookup URI.
344      *
345      * @param lookupUri The contact information to parse .
346      * @return The parsed {@code Contact} information.
347      * @throws JSONException
348      */
parseEncodedContactEntity(Uri lookupUri)349     public static Contact parseEncodedContactEntity(Uri lookupUri)  {
350         try {
351             return loadEncodedContactEntity(lookupUri, lookupUri);
352         } catch (JSONException je) {
353             return null;
354         }
355     }
356 
loadEncodedContactEntity(Uri uri, Uri lookupUri)357     private static Contact loadEncodedContactEntity(Uri uri, Uri lookupUri) throws JSONException {
358         final String jsonString = uri.getEncodedFragment();
359         final JSONObject json = new JSONObject(jsonString);
360 
361         final long directoryId =
362                 Long.valueOf(uri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY));
363 
364         final String displayName = json.optString(Contacts.DISPLAY_NAME);
365         final String altDisplayName = json.optString(
366                 Contacts.DISPLAY_NAME_ALTERNATIVE, displayName);
367         final int displayNameSource = json.getInt(Contacts.DISPLAY_NAME_SOURCE);
368         final String photoUri = json.optString(Contacts.PHOTO_URI, null);
369         final Contact contact = new Contact(
370                 uri, uri,
371                 lookupUri,
372                 directoryId,
373                 null /* lookupKey */,
374                 -1 /* id */,
375                 -1 /* nameRawContactId */,
376                 displayNameSource,
377                 0 /* photoId */,
378                 photoUri,
379                 displayName,
380                 altDisplayName,
381                 null /* phoneticName */,
382                 false /* starred */,
383                 null /* presence */,
384                 false /* sendToVoicemail */,
385                 null /* customRingtone */,
386                 false /* isUserProfile */);
387 
388         contact.setStatuses(new ImmutableMap.Builder<Long, DataStatus>().build());
389 
390         final String accountName = json.optString(RawContacts.ACCOUNT_NAME, null);
391         final String directoryName = uri.getQueryParameter(Directory.DISPLAY_NAME);
392         if (accountName != null) {
393             final String accountType = json.getString(RawContacts.ACCOUNT_TYPE);
394             contact.setDirectoryMetaData(directoryName, null, accountName, accountType,
395                     json.optInt(Directory.EXPORT_SUPPORT,
396                             Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY));
397         } else {
398             contact.setDirectoryMetaData(directoryName, null, null, null,
399                     json.optInt(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_ANY_ACCOUNT));
400         }
401 
402         final ContentValues values = new ContentValues();
403         values.put(Data._ID, -1);
404         values.put(Data.CONTACT_ID, -1);
405         final RawContact rawContact = new RawContact(values);
406 
407         final JSONObject items = json.getJSONObject(Contacts.CONTENT_ITEM_TYPE);
408         final Iterator keys = items.keys();
409         while (keys.hasNext()) {
410             final String mimetype = (String) keys.next();
411 
412             // Could be single object or array.
413             final JSONObject obj = items.optJSONObject(mimetype);
414             if (obj == null) {
415                 final JSONArray array = items.getJSONArray(mimetype);
416                 for (int i = 0; i < array.length(); i++) {
417                     final JSONObject item = array.getJSONObject(i);
418                     processOneRecord(rawContact, item, mimetype);
419                 }
420             } else {
421                 processOneRecord(rawContact, obj, mimetype);
422             }
423         }
424 
425         contact.setRawContacts(new ImmutableList.Builder<RawContact>()
426                 .add(rawContact)
427                 .build());
428         return contact;
429     }
430 
processOneRecord(RawContact rawContact, JSONObject item, String mimetype)431     private static void processOneRecord(RawContact rawContact, JSONObject item, String mimetype)
432             throws JSONException {
433         final ContentValues itemValues = new ContentValues();
434         itemValues.put(Data.MIMETYPE, mimetype);
435         itemValues.put(Data._ID, -1);
436 
437         final Iterator iterator = item.keys();
438         while (iterator.hasNext()) {
439             String name = (String) iterator.next();
440             final Object o = item.get(name);
441             if (o instanceof String) {
442                 itemValues.put(name, (String) o);
443             } else if (o instanceof Integer) {
444                 itemValues.put(name, (Integer) o);
445             }
446         }
447         rawContact.addDataItemValues(itemValues);
448     }
449 
loadContactEntity(ContentResolver resolver, Uri contactUri)450     private Contact loadContactEntity(ContentResolver resolver, Uri contactUri) {
451         Uri entityUri = Uri.withAppendedPath(contactUri, Contacts.Entity.CONTENT_DIRECTORY);
452         Cursor cursor = resolver.query(entityUri, ContactQuery.COLUMNS, null, null,
453                 Contacts.Entity.RAW_CONTACT_ID);
454         if (cursor == null) {
455             Log.e(TAG, "No cursor returned in loadContactEntity");
456             return Contact.forNotFound(mRequestedUri);
457         }
458 
459         try {
460             if (!cursor.moveToFirst()) {
461                 cursor.close();
462                 return Contact.forNotFound(mRequestedUri);
463             }
464 
465             // Create the loaded contact starting with the header data.
466             Contact contact = loadContactHeaderData(cursor, contactUri);
467 
468             // Fill in the raw contacts, which is wrapped in an Entity and any
469             // status data.  Initially, result has empty entities and statuses.
470             long currentRawContactId = -1;
471             RawContact rawContact = null;
472             ImmutableList.Builder<RawContact> rawContactsBuilder =
473                     new ImmutableList.Builder<RawContact>();
474             ImmutableMap.Builder<Long, DataStatus> statusesBuilder =
475                     new ImmutableMap.Builder<Long, DataStatus>();
476             do {
477                 long rawContactId = cursor.getLong(ContactQuery.RAW_CONTACT_ID);
478                 if (rawContactId != currentRawContactId) {
479                     // First time to see this raw contact id, so create a new entity, and
480                     // add it to the result's entities.
481                     currentRawContactId = rawContactId;
482                     rawContact = new RawContact(loadRawContactValues(cursor));
483                     rawContactsBuilder.add(rawContact);
484                 }
485                 if (!cursor.isNull(ContactQuery.DATA_ID)) {
486                     ContentValues data = loadDataValues(cursor);
487                     rawContact.addDataItemValues(data);
488 
489                     if (!cursor.isNull(ContactQuery.PRESENCE)
490                             || !cursor.isNull(ContactQuery.STATUS)) {
491                         final DataStatus status = new DataStatus(cursor);
492                         final long dataId = cursor.getLong(ContactQuery.DATA_ID);
493                         statusesBuilder.put(dataId, status);
494                     }
495                 }
496             } while (cursor.moveToNext());
497 
498             contact.setRawContacts(rawContactsBuilder.build());
499             contact.setStatuses(statusesBuilder.build());
500 
501             return contact;
502         } finally {
503             cursor.close();
504         }
505     }
506 
507     /**
508      * Looks for the photo data item in entities. If found, a thumbnail will be stored. A larger
509      * photo will also be stored if available.
510      */
loadPhotoBinaryData(Contact contactData)511     private void loadPhotoBinaryData(Contact contactData) {
512         loadThumbnailBinaryData(contactData);
513 
514         // Try to load the large photo from a file using the photo URI.
515         String photoUri = contactData.getPhotoUri();
516         if (photoUri != null) {
517             try {
518                 final InputStream inputStream;
519                 final AssetFileDescriptor fd;
520                 final Uri uri = Uri.parse(photoUri);
521                 final String scheme = uri.getScheme();
522                 if ("http".equals(scheme) || "https".equals(scheme)) {
523                     // Support HTTP urls that might come from extended directories
524                     inputStream = new URL(photoUri).openStream();
525                     fd = null;
526                 } else {
527                     fd = getContext().getContentResolver().openAssetFileDescriptor(uri, "r");
528                     inputStream = fd.createInputStream();
529                 }
530                 byte[] buffer = new byte[16 * 1024];
531                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
532                 try {
533                     int size;
534                     while ((size = inputStream.read(buffer)) != -1) {
535                         baos.write(buffer, 0, size);
536                     }
537                     contactData.setPhotoBinaryData(baos.toByteArray());
538                 } finally {
539                     inputStream.close();
540                     if (fd != null) {
541                         fd.close();
542                     }
543                 }
544                 return;
545             } catch (IOException ioe) {
546                 // Just fall back to the case below.
547             }
548         }
549 
550         // If we couldn't load from a file, fall back to the data blob.
551         contactData.setPhotoBinaryData(contactData.getThumbnailPhotoBinaryData());
552     }
553 
loadThumbnailBinaryData(Contact contactData)554     private void loadThumbnailBinaryData(Contact contactData) {
555         final long photoId = contactData.getPhotoId();
556         if (photoId <= 0) {
557             // No photo ID
558             return;
559         }
560 
561         for (RawContact rawContact : contactData.getRawContacts()) {
562             for (DataItem dataItem : rawContact.getDataItems()) {
563                 if (dataItem.getId() == photoId) {
564                     if (!(dataItem instanceof PhotoDataItem)) {
565                         break;
566                     }
567 
568                     final PhotoDataItem photo = (PhotoDataItem) dataItem;
569                     contactData.setThumbnailPhotoBinaryData(photo.getPhoto());
570                     break;
571                 }
572             }
573         }
574     }
575 
576     /**
577      * Extracts Contact level columns from the cursor.
578      */
loadContactHeaderData(final Cursor cursor, Uri contactUri)579     private Contact loadContactHeaderData(final Cursor cursor, Uri contactUri) {
580         final String directoryParameter =
581                 contactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
582         final long directoryId = directoryParameter == null
583                 ? Directory.DEFAULT
584                 : Long.parseLong(directoryParameter);
585         final long contactId = cursor.getLong(ContactQuery.CONTACT_ID);
586         final String lookupKey = cursor.getString(ContactQuery.LOOKUP_KEY);
587         final long nameRawContactId = cursor.getLong(ContactQuery.NAME_RAW_CONTACT_ID);
588         final int displayNameSource = cursor.getInt(ContactQuery.DISPLAY_NAME_SOURCE);
589         final String displayName = cursor.getString(ContactQuery.DISPLAY_NAME);
590         final String altDisplayName = cursor.getString(ContactQuery.ALT_DISPLAY_NAME);
591         final String phoneticName = cursor.getString(ContactQuery.PHONETIC_NAME);
592         final long photoId = cursor.getLong(ContactQuery.PHOTO_ID);
593         final String photoUri = cursor.getString(ContactQuery.PHOTO_URI);
594         final boolean starred = cursor.getInt(ContactQuery.STARRED) != 0;
595         final Integer presence = cursor.isNull(ContactQuery.CONTACT_PRESENCE)
596                 ? null
597                 : cursor.getInt(ContactQuery.CONTACT_PRESENCE);
598         final boolean sendToVoicemail = cursor.getInt(ContactQuery.SEND_TO_VOICEMAIL) == 1;
599         final String customRingtone = cursor.getString(ContactQuery.CUSTOM_RINGTONE);
600         final boolean isUserProfile = cursor.getInt(ContactQuery.IS_USER_PROFILE) == 1;
601 
602         Uri lookupUri;
603         if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) {
604             lookupUri = ContentUris.withAppendedId(
605                 Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), contactId);
606         } else {
607             lookupUri = contactUri;
608         }
609 
610         return new Contact(mRequestedUri, contactUri, lookupUri, directoryId, lookupKey,
611                 contactId, nameRawContactId, displayNameSource, photoId, photoUri, displayName,
612                 altDisplayName, phoneticName, starred, presence, sendToVoicemail,
613                 customRingtone, isUserProfile);
614     }
615 
616     /**
617      * Extracts RawContact level columns from the cursor.
618      */
loadRawContactValues(Cursor cursor)619     private ContentValues loadRawContactValues(Cursor cursor) {
620         ContentValues cv = new ContentValues();
621 
622         cv.put(RawContacts._ID, cursor.getLong(ContactQuery.RAW_CONTACT_ID));
623 
624         cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_NAME);
625         cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE);
626         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SET);
627         cursorColumnToContentValues(cursor, cv, ContactQuery.DIRTY);
628         cursorColumnToContentValues(cursor, cv, ContactQuery.VERSION);
629         cursorColumnToContentValues(cursor, cv, ContactQuery.SOURCE_ID);
630         cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC1);
631         cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC2);
632         cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC3);
633         cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC4);
634         cursorColumnToContentValues(cursor, cv, ContactQuery.DELETED);
635         cursorColumnToContentValues(cursor, cv, ContactQuery.CONTACT_ID);
636         cursorColumnToContentValues(cursor, cv, ContactQuery.STARRED);
637 
638         return cv;
639     }
640 
641     /**
642      * Extracts Data level columns from the cursor.
643      */
loadDataValues(Cursor cursor)644     private ContentValues loadDataValues(Cursor cursor) {
645         ContentValues cv = new ContentValues();
646 
647         cv.put(Data._ID, cursor.getLong(ContactQuery.DATA_ID));
648 
649         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA1);
650         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA2);
651         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA3);
652         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA4);
653         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA5);
654         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA6);
655         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA7);
656         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA8);
657         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA9);
658         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA10);
659         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA11);
660         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA12);
661         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA13);
662         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA14);
663         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA15);
664         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC1);
665         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC2);
666         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC3);
667         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC4);
668         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_VERSION);
669         cursorColumnToContentValues(cursor, cv, ContactQuery.IS_PRIMARY);
670         cursorColumnToContentValues(cursor, cv, ContactQuery.IS_SUPERPRIMARY);
671         cursorColumnToContentValues(cursor, cv, ContactQuery.MIMETYPE);
672         cursorColumnToContentValues(cursor, cv, ContactQuery.GROUP_SOURCE_ID);
673         cursorColumnToContentValues(cursor, cv, ContactQuery.CHAT_CAPABILITY);
674         if (CompatUtils.isMarshmallowCompatible()) {
675             cursorColumnToContentValues(cursor, cv, ContactQuery.CARRIER_PRESENCE);
676         }
677 
678         return cv;
679     }
680 
cursorColumnToContentValues( Cursor cursor, ContentValues values, int index)681     private void cursorColumnToContentValues(
682             Cursor cursor, ContentValues values, int index) {
683         switch (cursor.getType(index)) {
684             case Cursor.FIELD_TYPE_NULL:
685                 // don't put anything in the content values
686                 break;
687             case Cursor.FIELD_TYPE_INTEGER:
688                 values.put(ContactQuery.COLUMNS[index], cursor.getLong(index));
689                 break;
690             case Cursor.FIELD_TYPE_STRING:
691                 values.put(ContactQuery.COLUMNS[index], cursor.getString(index));
692                 break;
693             case Cursor.FIELD_TYPE_BLOB:
694                 values.put(ContactQuery.COLUMNS[index], cursor.getBlob(index));
695                 break;
696             default:
697                 throw new IllegalStateException("Invalid or unhandled data type");
698         }
699     }
700 
loadDirectoryMetaData(Contact result)701     private void loadDirectoryMetaData(Contact result) {
702         long directoryId = result.getDirectoryId();
703 
704         Cursor cursor = getContext().getContentResolver().query(
705                 ContentUris.withAppendedId(Directory.CONTENT_URI, directoryId),
706                 DirectoryQuery.COLUMNS, null, null, null);
707         if (cursor == null) {
708             return;
709         }
710         try {
711             if (cursor.moveToFirst()) {
712                 final String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME);
713                 final String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME);
714                 final int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID);
715                 final String accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
716                 final String accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
717                 final int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT);
718                 String directoryType = null;
719                 if (!TextUtils.isEmpty(packageName)) {
720                     PackageManager pm = getContext().getPackageManager();
721                     try {
722                         Resources resources = pm.getResourcesForApplication(packageName);
723                         directoryType = resources.getString(typeResourceId);
724                     } catch (NameNotFoundException e) {
725                         Log.w(TAG, "Contact directory resource not found: "
726                                 + packageName + "." + typeResourceId);
727                     }
728                 }
729 
730                 result.setDirectoryMetaData(
731                         displayName, directoryType, accountType, accountName, exportSupport);
732             }
733         } finally {
734             cursor.close();
735         }
736     }
737 
738     static private class AccountKey {
739         private final String mAccountName;
740         private final String mAccountType;
741         private final String mDataSet;
742 
AccountKey(String accountName, String accountType, String dataSet)743         public AccountKey(String accountName, String accountType, String dataSet) {
744             mAccountName = accountName;
745             mAccountType = accountType;
746             mDataSet = dataSet;
747         }
748 
749         @Override
hashCode()750         public int hashCode() {
751             return Objects.hash(mAccountName, mAccountType, mDataSet);
752         }
753 
754         @Override
equals(Object obj)755         public boolean equals(Object obj) {
756             if (!(obj instanceof AccountKey)) {
757                 return false;
758             }
759             final AccountKey other = (AccountKey) obj;
760             return Objects.equals(mAccountName, other.mAccountName)
761                 && Objects.equals(mAccountType, other.mAccountType)
762                 && Objects.equals(mDataSet, other.mDataSet);
763         }
764     }
765 
766     /**
767      * Loads groups meta-data for all groups associated with all constituent raw contacts'
768      * accounts.
769      */
loadGroupMetaData(Contact result)770     private void loadGroupMetaData(Contact result) {
771         StringBuilder selection = new StringBuilder();
772         ArrayList<String> selectionArgs = new ArrayList<String>();
773         final HashSet<AccountKey> accountsSeen = new HashSet<>();
774         for (RawContact rawContact : result.getRawContacts()) {
775             final String accountName = rawContact.getAccountName();
776             final String accountType = rawContact.getAccountTypeString();
777             final String dataSet = rawContact.getDataSet();
778             final AccountKey accountKey = new AccountKey(accountName, accountType, dataSet);
779             if (accountName != null && accountType != null &&
780                     !accountsSeen.contains(accountKey)) {
781                 accountsSeen.add(accountKey);
782                 if (selection.length() != 0) {
783                     selection.append(" OR ");
784                 }
785                 selection.append(
786                         "(" + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?");
787                 selectionArgs.add(accountName);
788                 selectionArgs.add(accountType);
789 
790                 selection.append(" AND " + Groups.DELETED + "=0");
791 
792                 if (dataSet != null) {
793                     selection.append(" AND " + Groups.DATA_SET + "=?");
794                     selectionArgs.add(dataSet);
795                 } else {
796                     selection.append(" AND " + Groups.DATA_SET + " IS NULL");
797                 }
798                 selection.append(")");
799             }
800         }
801         final ImmutableList.Builder<GroupMetaData> groupListBuilder = new ImmutableList.Builder<>();
802         final Cursor cursor = getContext().getContentResolver().query(Groups.CONTENT_URI,
803                 GroupMetaDataLoader.COLUMNS, selection.toString(),
804                 selectionArgs.toArray(new String[0]), null);
805         if (cursor != null) {
806             try {
807                 while (cursor.moveToNext()) {
808                     groupListBuilder.add(new GroupMetaData(getContext(), cursor));
809                 }
810             } finally {
811                 cursor.close();
812             }
813         }
814         result.setGroupMetaData(groupListBuilder.build());
815     }
816 
817     /**
818      * Iterates over all data items that represent phone numbers are tries to calculate a formatted
819      * number. This function can safely be called several times as no unformatted data is
820      * overwritten
821      */
computeFormattedPhoneNumbers(Contact contactData)822     private void computeFormattedPhoneNumbers(Contact contactData) {
823         final String countryIso = GeoUtil.getCurrentCountryIso(getContext());
824         final ImmutableList<RawContact> rawContacts = contactData.getRawContacts();
825         final int rawContactCount = rawContacts.size();
826         for (int rawContactIndex = 0; rawContactIndex < rawContactCount; rawContactIndex++) {
827             final RawContact rawContact = rawContacts.get(rawContactIndex);
828             final List<DataItem> dataItems = rawContact.getDataItems();
829             final int dataCount = dataItems.size();
830             for (int dataIndex = 0; dataIndex < dataCount; dataIndex++) {
831                 final DataItem dataItem = dataItems.get(dataIndex);
832                 if (dataItem instanceof PhoneDataItem) {
833                     final PhoneDataItem phoneDataItem = (PhoneDataItem) dataItem;
834                     phoneDataItem.computeFormattedPhoneNumber(countryIso);
835                 }
836             }
837         }
838     }
839 
840     @Override
deliverResult(Contact result)841     public void deliverResult(Contact result) {
842         unregisterObserver();
843 
844         // The creator isn't interested in any further updates
845         if (isReset() || result == null) {
846             return;
847         }
848 
849         mContact = result;
850 
851         if (result.isLoaded()) {
852             mLookupUri = result.getLookupUri();
853 
854             if (!result.isDirectoryEntry()) {
855                 Log.i(TAG, "Registering content observer for " + mLookupUri);
856                 if (mObserver == null) {
857                     mObserver = new ForceLoadContentObserver();
858                 }
859                 getContext().getContentResolver().registerContentObserver(
860                         mLookupUri, true, mObserver);
861             }
862 
863             if (mPostViewNotification) {
864                 // inform the source of the data that this contact is being looked at
865                 postViewNotificationToSyncAdapter();
866             }
867         }
868 
869         super.deliverResult(mContact);
870     }
871 
872     /**
873      * Posts a message to the contributing sync adapters that have opted-in, notifying them
874      * that the contact has just been loaded
875      */
postViewNotificationToSyncAdapter()876     private void postViewNotificationToSyncAdapter() {
877         Context context = getContext();
878         for (RawContact rawContact : mContact.getRawContacts()) {
879             final long rawContactId = rawContact.getId();
880             if (mNotifiedRawContactIds.contains(rawContactId)) {
881                 continue; // Already notified for this raw contact.
882             }
883             mNotifiedRawContactIds.add(rawContactId);
884             final AccountType accountType = rawContact.getAccountType(context);
885             final String serviceName = accountType.getViewContactNotifyServiceClassName();
886             final String servicePackageName = accountType
887                 .getViewContactNotifyServicePackageName();
888             if (!TextUtils.isEmpty(serviceName) && !TextUtils.isEmpty(servicePackageName)) {
889                 final Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
890                 if (accountType instanceof GoogleAccountType) {
891                     ((GoogleAccountType) accountType).handleRawContactViewed(context, uri);
892                     continue;
893                 }
894                 final Intent intent = new Intent();
895                 intent.setData(uri);
896                 // TODO: Social Stream API is deprecated, and once the opted-in
897                 // sync adapters target Android O+, we won't be able to start their services
898                 // since they'll likely be in the background, so we'll need to remove the
899                 // startService call.
900                 intent.setClassName(servicePackageName, serviceName);
901                 intent.setAction(Intent.ACTION_VIEW);
902                 try {
903                     context.startService(intent);
904                 } catch (Exception e) {
905                     Log.e(TAG, "Error sending message to source-app", e);
906                 }
907             }
908         }
909     }
910 
unregisterObserver()911     private void unregisterObserver() {
912         if (mObserver != null) {
913             getContext().getContentResolver().unregisterContentObserver(mObserver);
914             mObserver = null;
915         }
916     }
917 
getLookupUri()918     public Uri getLookupUri() {
919         return mLookupUri;
920     }
921 
922     @Override
onStartLoading()923     protected void onStartLoading() {
924         if (mContact != null) {
925             deliverResult(mContact);
926         }
927 
928         if (takeContentChanged() || mContact == null) {
929             forceLoad();
930         }
931     }
932 
933     @Override
onStopLoading()934     protected void onStopLoading() {
935         cancelLoad();
936     }
937 
938     @Override
onReset()939     protected void onReset() {
940         super.onReset();
941         cancelLoad();
942         unregisterObserver();
943         mContact = null;
944     }
945 
946     /**
947      * Caches the result, which is useful when we switch from activity to activity, using the same
948      * contact. If the next load is for a different contact, the cached result will be dropped
949      */
cacheResult()950     public void cacheResult() {
951         if (mContact == null || !mContact.isLoaded()) {
952             sCachedResult = null;
953         } else {
954             sCachedResult = mContact;
955         }
956     }
957 }
958