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