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