• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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;
18 
19 import com.android.contacts.model.AccountType;
20 import com.android.contacts.model.AccountTypeManager;
21 import com.android.contacts.model.AccountTypeWithDataSet;
22 import com.android.contacts.util.DataStatus;
23 import com.android.contacts.util.StreamItemEntry;
24 import com.android.contacts.util.StreamItemPhotoEntry;
25 import com.google.android.collect.Lists;
26 import com.google.common.annotations.VisibleForTesting;
27 import com.google.common.collect.Maps;
28 import com.google.common.collect.Sets;
29 
30 import android.content.ContentResolver;
31 import android.content.ContentUris;
32 import android.content.ContentValues;
33 import android.content.Context;
34 import android.content.Entity;
35 import android.content.Entity.NamedContentValues;
36 import android.content.Intent;
37 import android.content.Loader;
38 import android.content.pm.PackageManager;
39 import android.content.pm.PackageManager.NameNotFoundException;
40 import android.content.res.AssetFileDescriptor;
41 import android.content.res.Resources;
42 import android.database.Cursor;
43 import android.net.Uri;
44 import android.os.AsyncTask;
45 import android.provider.ContactsContract;
46 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
47 import android.provider.ContactsContract.CommonDataKinds.Photo;
48 import android.provider.ContactsContract.Contacts;
49 import android.provider.ContactsContract.Data;
50 import android.provider.ContactsContract.Directory;
51 import android.provider.ContactsContract.DisplayNameSources;
52 import android.provider.ContactsContract.Groups;
53 import android.provider.ContactsContract.RawContacts;
54 import android.provider.ContactsContract.StreamItemPhotos;
55 import android.provider.ContactsContract.StreamItems;
56 import android.text.TextUtils;
57 import android.util.Log;
58 
59 import java.io.ByteArrayOutputStream;
60 import java.io.FileInputStream;
61 import java.io.IOException;
62 import java.io.InputStream;
63 import java.util.ArrayList;
64 import java.util.Collections;
65 import java.util.HashMap;
66 import java.util.List;
67 import java.util.Map;
68 import java.util.Set;
69 
70 /**
71  * Loads a single Contact and all it constituent RawContacts.
72  */
73 public class ContactLoader extends Loader<ContactLoader.Result> {
74     private static final String TAG = "ContactLoader";
75 
76     private final Uri mRequestedUri;
77     private Uri mLookupUri;
78     private boolean mLoadGroupMetaData;
79     private boolean mLoadStreamItems;
80     private final boolean mLoadInvitableAccountTypes;
81     private Result mContact;
82     private ForceLoadContentObserver mObserver;
83     private boolean mDestroyed;
84     private final Set<Long> mNotifiedRawContactIds = Sets.newHashSet();
85 
86     public interface Listener {
onContactLoaded(Result contact)87         public void onContactLoaded(Result contact);
88     }
89 
90     /**
91      * The result of a load operation. Contains all data necessary to display the contact.
92      */
93     public static final class Result {
94         private enum Status {
95             /** Contact is successfully loaded */
96             LOADED,
97             /** There was an error loading the contact */
98             ERROR,
99             /** Contact is not found */
100             NOT_FOUND,
101         }
102 
103         private final Uri mRequestedUri;
104         private final Uri mLookupUri;
105         private final Uri mUri;
106         private final long mDirectoryId;
107         private final String mLookupKey;
108         private final long mId;
109         private final long mNameRawContactId;
110         private final int mDisplayNameSource;
111         private final long mPhotoId;
112         private final String mPhotoUri;
113         private final String mDisplayName;
114         private final String mAltDisplayName;
115         private final String mPhoneticName;
116         private final boolean mStarred;
117         private final Integer mPresence;
118         private final ArrayList<Entity> mEntities;
119         private final ArrayList<StreamItemEntry> mStreamItems;
120         private final HashMap<Long, DataStatus> mStatuses;
121         private final ArrayList<AccountType> mInvitableAccountTypes;
122 
123         private String mDirectoryDisplayName;
124         private String mDirectoryType;
125         private String mDirectoryAccountType;
126         private String mDirectoryAccountName;
127         private int mDirectoryExportSupport;
128 
129         private ArrayList<GroupMetaData> mGroups;
130 
131         private boolean mLoadingPhoto;
132         private byte[] mPhotoBinaryData;
133         private final boolean mSendToVoicemail;
134         private final String mCustomRingtone;
135         private final boolean mIsUserProfile;
136 
137         private final Status mStatus;
138         private final Exception mException;
139 
140         /**
141          * Constructor for special results, namely "no contact found" and "error".
142          */
Result(Uri requestedUri, Status status, Exception exception)143         private Result(Uri requestedUri, Status status, Exception exception) {
144             if (status == Status.ERROR && exception == null) {
145                 throw new IllegalArgumentException("ERROR result must have exception");
146             }
147             mStatus = status;
148             mException = exception;
149             mRequestedUri = requestedUri;
150             mLookupUri = null;
151             mUri = null;
152             mDirectoryId = -1;
153             mLookupKey = null;
154             mId = -1;
155             mEntities = null;
156             mStreamItems = new ArrayList<StreamItemEntry>();
157             mStatuses = null;
158             mNameRawContactId = -1;
159             mDisplayNameSource = DisplayNameSources.UNDEFINED;
160             mPhotoId = -1;
161             mPhotoUri = null;
162             mDisplayName = null;
163             mAltDisplayName = null;
164             mPhoneticName = null;
165             mStarred = false;
166             mPresence = null;
167             mInvitableAccountTypes = null;
168             mSendToVoicemail = false;
169             mCustomRingtone = null;
170             mIsUserProfile = false;
171         }
172 
forError(Uri requestedUri, Exception exception)173         private static Result forError(Uri requestedUri, Exception exception) {
174             return new Result(requestedUri, Status.ERROR, exception);
175         }
176 
forNotFound(Uri requestedUri)177         private static Result forNotFound(Uri requestedUri) {
178             return new Result(requestedUri, Status.NOT_FOUND, null);
179         }
180 
181         /**
182          * Constructor to call when contact was found
183          */
Result(Uri requestedUri, Uri uri, Uri lookupUri, long directoryId, String lookupKey, long id, long nameRawContactId, int displayNameSource, long photoId, String photoUri, String displayName, String altDisplayName, String phoneticName, boolean starred, Integer presence, boolean sendToVoicemail, String customRingtone, boolean isUserProfile)184         private Result(Uri requestedUri, Uri uri, Uri lookupUri, long directoryId, String lookupKey,
185                 long id, long nameRawContactId, int displayNameSource, long photoId,
186                 String photoUri, String displayName, String altDisplayName, String phoneticName,
187                 boolean starred, Integer presence, boolean sendToVoicemail, String customRingtone,
188                 boolean isUserProfile) {
189             mStatus = Status.LOADED;
190             mException = null;
191             mRequestedUri = requestedUri;
192             mLookupUri = lookupUri;
193             mUri = uri;
194             mDirectoryId = directoryId;
195             mLookupKey = lookupKey;
196             mId = id;
197             mEntities = new ArrayList<Entity>();
198             mStreamItems = new ArrayList<StreamItemEntry>();
199             mStatuses = new HashMap<Long, DataStatus>();
200             mNameRawContactId = nameRawContactId;
201             mDisplayNameSource = displayNameSource;
202             mPhotoId = photoId;
203             mPhotoUri = photoUri;
204             mDisplayName = displayName;
205             mAltDisplayName = altDisplayName;
206             mPhoneticName = phoneticName;
207             mStarred = starred;
208             mPresence = presence;
209             mInvitableAccountTypes = Lists.newArrayList();
210             mSendToVoicemail = sendToVoicemail;
211             mCustomRingtone = customRingtone;
212             mIsUserProfile = isUserProfile;
213         }
214 
Result(Result from)215         private Result(Result from) {
216             mStatus = from.mStatus;
217             mException = from.mException;
218             mRequestedUri = from.mRequestedUri;
219             mLookupUri = from.mLookupUri;
220             mUri = from.mUri;
221             mDirectoryId = from.mDirectoryId;
222             mLookupKey = from.mLookupKey;
223             mId = from.mId;
224             mNameRawContactId = from.mNameRawContactId;
225             mDisplayNameSource = from.mDisplayNameSource;
226             mPhotoId = from.mPhotoId;
227             mPhotoUri = from.mPhotoUri;
228             mDisplayName = from.mDisplayName;
229             mAltDisplayName = from.mAltDisplayName;
230             mPhoneticName = from.mPhoneticName;
231             mStarred = from.mStarred;
232             mPresence = from.mPresence;
233             mEntities = from.mEntities;
234             mStreamItems = from.mStreamItems;
235             mStatuses = from.mStatuses;
236             mInvitableAccountTypes = from.mInvitableAccountTypes;
237 
238             mDirectoryDisplayName = from.mDirectoryDisplayName;
239             mDirectoryType = from.mDirectoryType;
240             mDirectoryAccountType = from.mDirectoryAccountType;
241             mDirectoryAccountName = from.mDirectoryAccountName;
242             mDirectoryExportSupport = from.mDirectoryExportSupport;
243 
244             mGroups = from.mGroups;
245 
246             mLoadingPhoto = from.mLoadingPhoto;
247             mPhotoBinaryData = from.mPhotoBinaryData;
248             mSendToVoicemail = from.mSendToVoicemail;
249             mCustomRingtone = from.mCustomRingtone;
250             mIsUserProfile = from.mIsUserProfile;
251         }
252 
253         /**
254          * @param exportSupport See {@link Directory#EXPORT_SUPPORT}.
255          */
setDirectoryMetaData(String displayName, String directoryType, String accountType, String accountName, int exportSupport)256         private void setDirectoryMetaData(String displayName, String directoryType,
257                 String accountType, String accountName, int exportSupport) {
258             mDirectoryDisplayName = displayName;
259             mDirectoryType = directoryType;
260             mDirectoryAccountType = accountType;
261             mDirectoryAccountName = accountName;
262             mDirectoryExportSupport = exportSupport;
263         }
264 
setLoadingPhoto(boolean flag)265         private void setLoadingPhoto(boolean flag) {
266             mLoadingPhoto = flag;
267         }
268 
setPhotoBinaryData(byte[] photoBinaryData)269         private void setPhotoBinaryData(byte[] photoBinaryData) {
270             mPhotoBinaryData = photoBinaryData;
271         }
272 
273         /**
274          * Returns the URI for the contact that contains both the lookup key and the ID. This is
275          * the best URI to reference a contact.
276          * For directory contacts, this is the same a the URI as returned by {@link #getUri()}
277          */
getLookupUri()278         public Uri getLookupUri() {
279             return mLookupUri;
280         }
281 
getLookupKey()282         public String getLookupKey() {
283             return mLookupKey;
284         }
285 
286         /**
287          * Returns the contact Uri that was passed to the provider to make the query. This is
288          * the same as the requested Uri, unless the requested Uri doesn't specify a Contact:
289          * If it either references a Raw-Contact or a Person (a pre-Eclair style Uri), this Uri will
290          * always reference the full aggregate contact.
291          */
getUri()292         public Uri getUri() {
293             return mUri;
294         }
295 
296         /**
297          * Returns the URI for which this {@link ContactLoader) was initially requested.
298          */
getRequestedUri()299         public Uri getRequestedUri() {
300             return mRequestedUri;
301         }
302 
303         @VisibleForTesting
getId()304         /*package*/ long getId() {
305             return mId;
306         }
307 
308         /**
309          * @return true when an exception happened during loading, in which case
310          *     {@link #getException} returns the actual exception object.
311          *     Note {@link #isNotFound()} and {@link #isError()} are mutually exclusive; If
312          *     {@link #isError()} is {@code true}, {@link #isNotFound()} is always {@code false},
313          *     and vice versa.
314          */
isError()315         public boolean isError() {
316             return mStatus == Status.ERROR;
317         }
318 
getException()319         public Exception getException() {
320             return mException;
321         }
322 
323         /**
324          * @return true when the specified contact is not found.
325          *     Note {@link #isNotFound()} and {@link #isError()} are mutually exclusive; If
326          *     {@link #isError()} is {@code true}, {@link #isNotFound()} is always {@code false},
327          *     and vice versa.
328          */
isNotFound()329         public boolean isNotFound() {
330             return mStatus == Status.NOT_FOUND;
331         }
332 
333         /**
334          * @return true if the specified contact is successfully loaded.
335          *     i.e. neither {@link #isError()} nor {@link #isNotFound()}.
336          */
isLoaded()337         public boolean isLoaded() {
338             return mStatus == Status.LOADED;
339         }
340 
getNameRawContactId()341         public long getNameRawContactId() {
342             return mNameRawContactId;
343         }
344 
getDisplayNameSource()345         public int getDisplayNameSource() {
346             return mDisplayNameSource;
347         }
348 
getPhotoId()349         public long getPhotoId() {
350             return mPhotoId;
351         }
352 
getPhotoUri()353         public String getPhotoUri() {
354             return mPhotoUri;
355         }
356 
getDisplayName()357         public String getDisplayName() {
358             return mDisplayName;
359         }
360 
getAltDisplayName()361         public String getAltDisplayName() {
362             return mAltDisplayName;
363         }
364 
getPhoneticName()365         public String getPhoneticName() {
366             return mPhoneticName;
367         }
368 
getStarred()369         public boolean getStarred() {
370             return mStarred;
371         }
372 
getPresence()373         public Integer getPresence() {
374             return mPresence;
375         }
376 
getInvitableAccountTypes()377         public ArrayList<AccountType> getInvitableAccountTypes() {
378             return mInvitableAccountTypes;
379         }
380 
getEntities()381         public ArrayList<Entity> getEntities() {
382             return mEntities;
383         }
384 
getStreamItems()385         public ArrayList<StreamItemEntry> getStreamItems() {
386             return mStreamItems;
387         }
388 
getStatuses()389         public HashMap<Long, DataStatus> getStatuses() {
390             return mStatuses;
391         }
392 
getDirectoryId()393         public long getDirectoryId() {
394             return mDirectoryId;
395         }
396 
isDirectoryEntry()397         public boolean isDirectoryEntry() {
398             return mDirectoryId != -1 && mDirectoryId != Directory.DEFAULT
399                     && mDirectoryId != Directory.LOCAL_INVISIBLE;
400         }
401 
getDirectoryExportSupport()402         public int getDirectoryExportSupport() {
403             return mDirectoryExportSupport;
404         }
405 
getDirectoryDisplayName()406         public String getDirectoryDisplayName() {
407             return mDirectoryDisplayName;
408         }
409 
getDirectoryType()410         public String getDirectoryType() {
411             return mDirectoryType;
412         }
413 
getDirectoryAccountType()414         public String getDirectoryAccountType() {
415             return mDirectoryAccountType;
416         }
417 
getDirectoryAccountName()418         public String getDirectoryAccountName() {
419             return mDirectoryAccountName;
420         }
421 
isLoadingPhoto()422         public boolean isLoadingPhoto() {
423             return mLoadingPhoto;
424         }
425 
getPhotoBinaryData()426         public byte[] getPhotoBinaryData() {
427             return mPhotoBinaryData;
428         }
429 
getContentValues()430         public ArrayList<ContentValues> getContentValues() {
431             if (mEntities.size() != 1) {
432                 throw new IllegalStateException(
433                         "Cannot extract content values from an aggregated contact");
434             }
435 
436             Entity entity = mEntities.get(0);
437             ArrayList<ContentValues> result = new ArrayList<ContentValues>();
438             ArrayList<NamedContentValues> subValues = entity.getSubValues();
439             if (subValues != null) {
440                 int size = subValues.size();
441                 for (int i = 0; i < size; i++) {
442                     NamedContentValues pair = subValues.get(i);
443                     if (Data.CONTENT_URI.equals(pair.uri)) {
444                         result.add(pair.values);
445                     }
446                 }
447             }
448 
449             // If the photo was loaded using the URI, create an entry for the photo
450             // binary data.
451             if (mPhotoId == 0 && mPhotoBinaryData != null) {
452                 ContentValues photo = new ContentValues();
453                 photo.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
454                 photo.put(Photo.PHOTO, mPhotoBinaryData);
455                 result.add(photo);
456             }
457 
458             return result;
459         }
460 
addGroupMetaData(GroupMetaData group)461         private void addGroupMetaData(GroupMetaData group) {
462             if (mGroups == null) {
463                 mGroups = new ArrayList<GroupMetaData>();
464             }
465             mGroups.add(group);
466         }
467 
getGroupMetaData()468         public List<GroupMetaData> getGroupMetaData() {
469             return mGroups;
470         }
471 
isSendToVoicemail()472         public boolean isSendToVoicemail() {
473             return mSendToVoicemail;
474         }
475 
getCustomRingtone()476         public String getCustomRingtone() {
477             return mCustomRingtone;
478         }
479 
isUserProfile()480         public boolean isUserProfile() {
481             return mIsUserProfile;
482         }
483     }
484 
485     /**
486      * Projection used for the query that loads all data for the entire contact (except for
487      * social stream items).
488      */
489     private static class ContactQuery {
490         final static String[] COLUMNS = new String[] {
491                 Contacts.NAME_RAW_CONTACT_ID,
492                 Contacts.DISPLAY_NAME_SOURCE,
493                 Contacts.LOOKUP_KEY,
494                 Contacts.DISPLAY_NAME,
495                 Contacts.DISPLAY_NAME_ALTERNATIVE,
496                 Contacts.PHONETIC_NAME,
497                 Contacts.PHOTO_ID,
498                 Contacts.STARRED,
499                 Contacts.CONTACT_PRESENCE,
500                 Contacts.CONTACT_STATUS,
501                 Contacts.CONTACT_STATUS_TIMESTAMP,
502                 Contacts.CONTACT_STATUS_RES_PACKAGE,
503                 Contacts.CONTACT_STATUS_LABEL,
504                 Contacts.Entity.CONTACT_ID,
505                 Contacts.Entity.RAW_CONTACT_ID,
506 
507                 RawContacts.ACCOUNT_NAME,
508                 RawContacts.ACCOUNT_TYPE,
509                 RawContacts.DATA_SET,
510                 RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
511                 RawContacts.DIRTY,
512                 RawContacts.VERSION,
513                 RawContacts.SOURCE_ID,
514                 RawContacts.SYNC1,
515                 RawContacts.SYNC2,
516                 RawContacts.SYNC3,
517                 RawContacts.SYNC4,
518                 RawContacts.DELETED,
519                 RawContacts.NAME_VERIFIED,
520 
521                 Contacts.Entity.DATA_ID,
522                 Data.DATA1,
523                 Data.DATA2,
524                 Data.DATA3,
525                 Data.DATA4,
526                 Data.DATA5,
527                 Data.DATA6,
528                 Data.DATA7,
529                 Data.DATA8,
530                 Data.DATA9,
531                 Data.DATA10,
532                 Data.DATA11,
533                 Data.DATA12,
534                 Data.DATA13,
535                 Data.DATA14,
536                 Data.DATA15,
537                 Data.SYNC1,
538                 Data.SYNC2,
539                 Data.SYNC3,
540                 Data.SYNC4,
541                 Data.DATA_VERSION,
542                 Data.IS_PRIMARY,
543                 Data.IS_SUPER_PRIMARY,
544                 Data.MIMETYPE,
545                 Data.RES_PACKAGE,
546 
547                 GroupMembership.GROUP_SOURCE_ID,
548 
549                 Data.PRESENCE,
550                 Data.CHAT_CAPABILITY,
551                 Data.STATUS,
552                 Data.STATUS_RES_PACKAGE,
553                 Data.STATUS_ICON,
554                 Data.STATUS_LABEL,
555                 Data.STATUS_TIMESTAMP,
556 
557                 Contacts.PHOTO_URI,
558                 Contacts.SEND_TO_VOICEMAIL,
559                 Contacts.CUSTOM_RINGTONE,
560                 Contacts.IS_USER_PROFILE,
561         };
562 
563         public final static int NAME_RAW_CONTACT_ID = 0;
564         public final static int DISPLAY_NAME_SOURCE = 1;
565         public final static int LOOKUP_KEY = 2;
566         public final static int DISPLAY_NAME = 3;
567         public final static int ALT_DISPLAY_NAME = 4;
568         public final static int PHONETIC_NAME = 5;
569         public final static int PHOTO_ID = 6;
570         public final static int STARRED = 7;
571         public final static int CONTACT_PRESENCE = 8;
572         public final static int CONTACT_STATUS = 9;
573         public final static int CONTACT_STATUS_TIMESTAMP = 10;
574         public final static int CONTACT_STATUS_RES_PACKAGE = 11;
575         public final static int CONTACT_STATUS_LABEL = 12;
576         public final static int CONTACT_ID = 13;
577         public final static int RAW_CONTACT_ID = 14;
578 
579         public final static int ACCOUNT_NAME = 15;
580         public final static int ACCOUNT_TYPE = 16;
581         public final static int DATA_SET = 17;
582         public final static int ACCOUNT_TYPE_AND_DATA_SET = 18;
583         public final static int DIRTY = 19;
584         public final static int VERSION = 20;
585         public final static int SOURCE_ID = 21;
586         public final static int SYNC1 = 22;
587         public final static int SYNC2 = 23;
588         public final static int SYNC3 = 24;
589         public final static int SYNC4 = 25;
590         public final static int DELETED = 26;
591         public final static int NAME_VERIFIED = 27;
592 
593         public final static int DATA_ID = 28;
594         public final static int DATA1 = 29;
595         public final static int DATA2 = 30;
596         public final static int DATA3 = 31;
597         public final static int DATA4 = 32;
598         public final static int DATA5 = 33;
599         public final static int DATA6 = 34;
600         public final static int DATA7 = 35;
601         public final static int DATA8 = 36;
602         public final static int DATA9 = 37;
603         public final static int DATA10 = 38;
604         public final static int DATA11 = 39;
605         public final static int DATA12 = 40;
606         public final static int DATA13 = 41;
607         public final static int DATA14 = 42;
608         public final static int DATA15 = 43;
609         public final static int DATA_SYNC1 = 44;
610         public final static int DATA_SYNC2 = 45;
611         public final static int DATA_SYNC3 = 46;
612         public final static int DATA_SYNC4 = 47;
613         public final static int DATA_VERSION = 48;
614         public final static int IS_PRIMARY = 49;
615         public final static int IS_SUPERPRIMARY = 50;
616         public final static int MIMETYPE = 51;
617         public final static int RES_PACKAGE = 52;
618 
619         public final static int GROUP_SOURCE_ID = 53;
620 
621         public final static int PRESENCE = 54;
622         public final static int CHAT_CAPABILITY = 55;
623         public final static int STATUS = 56;
624         public final static int STATUS_RES_PACKAGE = 57;
625         public final static int STATUS_ICON = 58;
626         public final static int STATUS_LABEL = 59;
627         public final static int STATUS_TIMESTAMP = 60;
628 
629         public final static int PHOTO_URI = 61;
630         public final static int SEND_TO_VOICEMAIL = 62;
631         public final static int CUSTOM_RINGTONE = 63;
632         public final static int IS_USER_PROFILE = 64;
633     }
634 
635     /**
636      * Projection used for the query that loads all data for the entire contact.
637      */
638     private static class DirectoryQuery {
639         final static String[] COLUMNS = new String[] {
640             Directory.DISPLAY_NAME,
641             Directory.PACKAGE_NAME,
642             Directory.TYPE_RESOURCE_ID,
643             Directory.ACCOUNT_TYPE,
644             Directory.ACCOUNT_NAME,
645             Directory.EXPORT_SUPPORT,
646         };
647 
648         public final static int DISPLAY_NAME = 0;
649         public final static int PACKAGE_NAME = 1;
650         public final static int TYPE_RESOURCE_ID = 2;
651         public final static int ACCOUNT_TYPE = 3;
652         public final static int ACCOUNT_NAME = 4;
653         public final static int EXPORT_SUPPORT = 5;
654     }
655 
656     private static class GroupQuery {
657         final static String[] COLUMNS = new String[] {
658             Groups.ACCOUNT_NAME,
659             Groups.ACCOUNT_TYPE,
660             Groups.DATA_SET,
661             Groups.ACCOUNT_TYPE_AND_DATA_SET,
662             Groups._ID,
663             Groups.TITLE,
664             Groups.AUTO_ADD,
665             Groups.FAVORITES,
666         };
667 
668         public final static int ACCOUNT_NAME = 0;
669         public final static int ACCOUNT_TYPE = 1;
670         public final static int DATA_SET = 2;
671         public final static int ACCOUNT_TYPE_AND_DATA_SET = 3;
672         public final static int ID = 4;
673         public final static int TITLE = 5;
674         public final static int AUTO_ADD = 6;
675         public final static int FAVORITES = 7;
676     }
677 
678     private final class LoadContactTask extends AsyncTask<Void, Void, Result> {
679 
680         @Override
doInBackground(Void... args)681         protected Result doInBackground(Void... args) {
682             try {
683                 final ContentResolver resolver = getContext().getContentResolver();
684                 final Uri uriCurrentFormat = ensureIsContactUri(resolver, mLookupUri);
685                 Result result = loadContactEntity(resolver, uriCurrentFormat);
686                 if (!result.isNotFound()) {
687                     if (result.isDirectoryEntry()) {
688                         loadDirectoryMetaData(result);
689                     } else if (mLoadGroupMetaData) {
690                         loadGroupMetaData(result);
691                     }
692                     if (mLoadStreamItems) {
693                         loadStreamItems(result);
694                     }
695                     loadPhotoBinaryData(result);
696 
697                     // Note ME profile should never have "Add connection"
698                     if (mLoadInvitableAccountTypes && !result.isUserProfile()) {
699                         loadInvitableAccountTypes(result);
700                     }
701                 }
702                 return result;
703             } catch (Exception e) {
704                 Log.e(TAG, "Error loading the contact: " + mLookupUri, e);
705                 return Result.forError(mRequestedUri, e);
706             }
707         }
708 
709         /**
710          * Transforms the given Uri and returns a Lookup-Uri that represents the contact.
711          * For legacy contacts, a raw-contact lookup is performed.
712          * @param resolver
713          */
ensureIsContactUri(final ContentResolver resolver, final Uri uri)714         private Uri ensureIsContactUri(final ContentResolver resolver, final Uri uri) {
715             if (uri == null) throw new IllegalArgumentException("uri must not be null");
716 
717             final String authority = uri.getAuthority();
718 
719             // Current Style Uri?
720             if (ContactsContract.AUTHORITY.equals(authority)) {
721                 final String type = resolver.getType(uri);
722                 // Contact-Uri? Good, return it
723                 if (Contacts.CONTENT_ITEM_TYPE.equals(type)) {
724                     return uri;
725                 }
726 
727                 // RawContact-Uri? Transform it to ContactUri
728                 if (RawContacts.CONTENT_ITEM_TYPE.equals(type)) {
729                     final long rawContactId = ContentUris.parseId(uri);
730                     return RawContacts.getContactLookupUri(getContext().getContentResolver(),
731                             ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
732                 }
733 
734                 // Anything else? We don't know what this is
735                 throw new IllegalArgumentException("uri format is unknown");
736             }
737 
738             // Legacy Style? Convert to RawContact
739             final String OBSOLETE_AUTHORITY = "contacts";
740             if (OBSOLETE_AUTHORITY.equals(authority)) {
741                 // Legacy Format. Convert to RawContact-Uri and then lookup the contact
742                 final long rawContactId = ContentUris.parseId(uri);
743                 return RawContacts.getContactLookupUri(resolver,
744                         ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
745             }
746 
747             throw new IllegalArgumentException("uri authority is unknown");
748         }
749 
loadContactEntity(ContentResolver resolver, Uri contactUri)750         private Result loadContactEntity(ContentResolver resolver, Uri contactUri) {
751             Uri entityUri = Uri.withAppendedPath(contactUri, Contacts.Entity.CONTENT_DIRECTORY);
752             Cursor cursor = resolver.query(entityUri, ContactQuery.COLUMNS, null, null,
753                     Contacts.Entity.RAW_CONTACT_ID);
754             if (cursor == null) {
755                 Log.e(TAG, "No cursor returned in loadContactEntity");
756                 return Result.forNotFound(mRequestedUri);
757             }
758 
759             try {
760                 if (!cursor.moveToFirst()) {
761                     cursor.close();
762                     return Result.forNotFound(mRequestedUri);
763                 }
764 
765                 long currentRawContactId = -1;
766                 Entity entity = null;
767                 Result result = loadContactHeaderData(cursor, contactUri);
768                 ArrayList<Entity> entities = result.getEntities();
769                 HashMap<Long, DataStatus> statuses = result.getStatuses();
770                 for (; !cursor.isAfterLast(); cursor.moveToNext()) {
771                     long rawContactId = cursor.getLong(ContactQuery.RAW_CONTACT_ID);
772                     if (rawContactId != currentRawContactId) {
773                         currentRawContactId = rawContactId;
774                         entity = new android.content.Entity(loadRawContact(cursor));
775                         entities.add(entity);
776                     }
777                     if (!cursor.isNull(ContactQuery.DATA_ID)) {
778                         ContentValues data = loadData(cursor);
779                         entity.addSubValue(ContactsContract.Data.CONTENT_URI, data);
780 
781                         if (!cursor.isNull(ContactQuery.PRESENCE)
782                                 || !cursor.isNull(ContactQuery.STATUS)) {
783                             final DataStatus status = new DataStatus(cursor);
784                             final long dataId = cursor.getLong(ContactQuery.DATA_ID);
785                             statuses.put(dataId, status);
786                         }
787                     }
788                 }
789 
790                 return result;
791             } finally {
792                 cursor.close();
793             }
794         }
795 
796         /**
797          * Looks for the photo data item in entities. If found, creates a new Bitmap instance. If
798          * not found, returns null
799          */
loadPhotoBinaryData(Result contactData)800         private void loadPhotoBinaryData(Result contactData) {
801 
802             // If we have a photo URI, try loading that first.
803             String photoUri = contactData.getPhotoUri();
804             if (photoUri != null) {
805                 try {
806                     AssetFileDescriptor fd = getContext().getContentResolver()
807                            .openAssetFileDescriptor(Uri.parse(photoUri), "r");
808                     byte[] buffer = new byte[16 * 1024];
809                     FileInputStream fis = fd.createInputStream();
810                     ByteArrayOutputStream baos = new ByteArrayOutputStream();
811                     try {
812                         int size;
813                         while ((size = fis.read(buffer)) != -1) {
814                             baos.write(buffer, 0, size);
815                         }
816                         contactData.setPhotoBinaryData(baos.toByteArray());
817                     } finally {
818                         fis.close();
819                         fd.close();
820                     }
821                     return;
822                 } catch (IOException ioe) {
823                     // Just fall back to the case below.
824                 }
825             }
826 
827             // If we couldn't load from a file, fall back to the data blob.
828             final long photoId = contactData.getPhotoId();
829             if (photoId <= 0) {
830                 // No photo ID
831                 return;
832             }
833 
834             for (Entity entity : contactData.getEntities()) {
835                 for (NamedContentValues subValue : entity.getSubValues()) {
836                     final ContentValues entryValues = subValue.values;
837                     final long dataId = entryValues.getAsLong(Data._ID);
838                     if (dataId == photoId) {
839                         final String mimeType = entryValues.getAsString(Data.MIMETYPE);
840                         // Correct Data Id but incorrect MimeType? Don't load
841                         if (!Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
842                             return;
843                         }
844                         contactData.setPhotoBinaryData(entryValues.getAsByteArray(Photo.PHOTO));
845                         break;
846                     }
847                 }
848             }
849         }
850 
851         /**
852          * Sets the "invitable" account types to {@link Result#mInvitableAccountTypes}.
853          */
loadInvitableAccountTypes(Result contactData)854         private void loadInvitableAccountTypes(Result contactData) {
855             Map<AccountTypeWithDataSet, AccountType> invitables =
856                     AccountTypeManager.getInstance(getContext()).getUsableInvitableAccountTypes();
857             if (invitables.isEmpty()) {
858                 return;
859             }
860 
861             HashMap<AccountTypeWithDataSet, AccountType> result = Maps.newHashMap(invitables);
862 
863             // Remove the ones that already have a raw contact in the current contact
864             for (Entity entity : contactData.getEntities()) {
865                 final ContentValues values = entity.getEntityValues();
866                 final AccountTypeWithDataSet type = AccountTypeWithDataSet.get(
867                         values.getAsString(RawContacts.ACCOUNT_TYPE),
868                         values.getAsString(RawContacts.DATA_SET));
869                 result.remove(type);
870             }
871 
872             // Set to mInvitableAccountTypes
873             contactData.mInvitableAccountTypes.addAll(result.values());
874         }
875 
876         /**
877          * Extracts Contact level columns from the cursor.
878          */
loadContactHeaderData(final Cursor cursor, Uri contactUri)879         private Result loadContactHeaderData(final Cursor cursor, Uri contactUri) {
880             final String directoryParameter =
881                     contactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
882             final long directoryId = directoryParameter == null
883                     ? Directory.DEFAULT
884                     : Long.parseLong(directoryParameter);
885             final long contactId = cursor.getLong(ContactQuery.CONTACT_ID);
886             final String lookupKey = cursor.getString(ContactQuery.LOOKUP_KEY);
887             final long nameRawContactId = cursor.getLong(ContactQuery.NAME_RAW_CONTACT_ID);
888             final int displayNameSource = cursor.getInt(ContactQuery.DISPLAY_NAME_SOURCE);
889             final String displayName = cursor.getString(ContactQuery.DISPLAY_NAME);
890             final String altDisplayName = cursor.getString(ContactQuery.ALT_DISPLAY_NAME);
891             final String phoneticName = cursor.getString(ContactQuery.PHONETIC_NAME);
892             final long photoId = cursor.getLong(ContactQuery.PHOTO_ID);
893             final String photoUri = cursor.getString(ContactQuery.PHOTO_URI);
894             final boolean starred = cursor.getInt(ContactQuery.STARRED) != 0;
895             final Integer presence = cursor.isNull(ContactQuery.CONTACT_PRESENCE)
896                     ? null
897                     : cursor.getInt(ContactQuery.CONTACT_PRESENCE);
898             final boolean sendToVoicemail = cursor.getInt(ContactQuery.SEND_TO_VOICEMAIL) == 1;
899             final String customRingtone = cursor.getString(ContactQuery.CUSTOM_RINGTONE);
900             final boolean isUserProfile = cursor.getInt(ContactQuery.IS_USER_PROFILE) == 1;
901 
902             Uri lookupUri;
903             if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) {
904                 lookupUri = ContentUris.withAppendedId(
905                     Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), contactId);
906             } else {
907                 lookupUri = contactUri;
908             }
909 
910             return new Result(mRequestedUri, contactUri, lookupUri, directoryId, lookupKey,
911                     contactId, nameRawContactId, displayNameSource, photoId, photoUri, displayName,
912                     altDisplayName, phoneticName, starred, presence, sendToVoicemail,
913                     customRingtone, isUserProfile);
914         }
915 
916         /**
917          * Extracts RawContact level columns from the cursor.
918          */
loadRawContact(Cursor cursor)919         private ContentValues loadRawContact(Cursor cursor) {
920             ContentValues cv = new ContentValues();
921 
922             cv.put(RawContacts._ID, cursor.getLong(ContactQuery.RAW_CONTACT_ID));
923 
924             cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_NAME);
925             cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE);
926             cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SET);
927             cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE_AND_DATA_SET);
928             cursorColumnToContentValues(cursor, cv, ContactQuery.DIRTY);
929             cursorColumnToContentValues(cursor, cv, ContactQuery.VERSION);
930             cursorColumnToContentValues(cursor, cv, ContactQuery.SOURCE_ID);
931             cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC1);
932             cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC2);
933             cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC3);
934             cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC4);
935             cursorColumnToContentValues(cursor, cv, ContactQuery.DELETED);
936             cursorColumnToContentValues(cursor, cv, ContactQuery.CONTACT_ID);
937             cursorColumnToContentValues(cursor, cv, ContactQuery.STARRED);
938             cursorColumnToContentValues(cursor, cv, ContactQuery.NAME_VERIFIED);
939 
940             return cv;
941         }
942 
943         /**
944          * Extracts Data level columns from the cursor.
945          */
loadData(Cursor cursor)946         private ContentValues loadData(Cursor cursor) {
947             ContentValues cv = new ContentValues();
948 
949             cv.put(Data._ID, cursor.getLong(ContactQuery.DATA_ID));
950 
951             cursorColumnToContentValues(cursor, cv, ContactQuery.DATA1);
952             cursorColumnToContentValues(cursor, cv, ContactQuery.DATA2);
953             cursorColumnToContentValues(cursor, cv, ContactQuery.DATA3);
954             cursorColumnToContentValues(cursor, cv, ContactQuery.DATA4);
955             cursorColumnToContentValues(cursor, cv, ContactQuery.DATA5);
956             cursorColumnToContentValues(cursor, cv, ContactQuery.DATA6);
957             cursorColumnToContentValues(cursor, cv, ContactQuery.DATA7);
958             cursorColumnToContentValues(cursor, cv, ContactQuery.DATA8);
959             cursorColumnToContentValues(cursor, cv, ContactQuery.DATA9);
960             cursorColumnToContentValues(cursor, cv, ContactQuery.DATA10);
961             cursorColumnToContentValues(cursor, cv, ContactQuery.DATA11);
962             cursorColumnToContentValues(cursor, cv, ContactQuery.DATA12);
963             cursorColumnToContentValues(cursor, cv, ContactQuery.DATA13);
964             cursorColumnToContentValues(cursor, cv, ContactQuery.DATA14);
965             cursorColumnToContentValues(cursor, cv, ContactQuery.DATA15);
966             cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC1);
967             cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC2);
968             cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC3);
969             cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC4);
970             cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_VERSION);
971             cursorColumnToContentValues(cursor, cv, ContactQuery.IS_PRIMARY);
972             cursorColumnToContentValues(cursor, cv, ContactQuery.IS_SUPERPRIMARY);
973             cursorColumnToContentValues(cursor, cv, ContactQuery.MIMETYPE);
974             cursorColumnToContentValues(cursor, cv, ContactQuery.RES_PACKAGE);
975             cursorColumnToContentValues(cursor, cv, ContactQuery.GROUP_SOURCE_ID);
976             cursorColumnToContentValues(cursor, cv, ContactQuery.CHAT_CAPABILITY);
977 
978             return cv;
979         }
980 
cursorColumnToContentValues( Cursor cursor, ContentValues values, int index)981         private void cursorColumnToContentValues(
982                 Cursor cursor, ContentValues values, int index) {
983             switch (cursor.getType(index)) {
984                 case Cursor.FIELD_TYPE_NULL:
985                     // don't put anything in the content values
986                     break;
987                 case Cursor.FIELD_TYPE_INTEGER:
988                     values.put(ContactQuery.COLUMNS[index], cursor.getLong(index));
989                     break;
990                 case Cursor.FIELD_TYPE_STRING:
991                     values.put(ContactQuery.COLUMNS[index], cursor.getString(index));
992                     break;
993                 case Cursor.FIELD_TYPE_BLOB:
994                     values.put(ContactQuery.COLUMNS[index], cursor.getBlob(index));
995                     break;
996                 default:
997                     throw new IllegalStateException("Invalid or unhandled data type");
998             }
999         }
1000 
loadDirectoryMetaData(Result result)1001         private void loadDirectoryMetaData(Result result) {
1002             long directoryId = result.getDirectoryId();
1003 
1004             Cursor cursor = getContext().getContentResolver().query(
1005                     ContentUris.withAppendedId(Directory.CONTENT_URI, directoryId),
1006                     DirectoryQuery.COLUMNS, null, null, null);
1007             if (cursor == null) {
1008                 return;
1009             }
1010             try {
1011                 if (cursor.moveToFirst()) {
1012                     final String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME);
1013                     final String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME);
1014                     final int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID);
1015                     final String accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
1016                     final String accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
1017                     final int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT);
1018                     String directoryType = null;
1019                     if (!TextUtils.isEmpty(packageName)) {
1020                         PackageManager pm = getContext().getPackageManager();
1021                         try {
1022                             Resources resources = pm.getResourcesForApplication(packageName);
1023                             directoryType = resources.getString(typeResourceId);
1024                         } catch (NameNotFoundException e) {
1025                             Log.w(TAG, "Contact directory resource not found: "
1026                                     + packageName + "." + typeResourceId);
1027                         }
1028                     }
1029 
1030                     result.setDirectoryMetaData(
1031                             displayName, directoryType, accountType, accountName, exportSupport);
1032                 }
1033             } finally {
1034                 cursor.close();
1035             }
1036         }
1037 
1038         /**
1039          * Loads groups meta-data for all groups associated with all constituent raw contacts'
1040          * accounts.
1041          */
loadGroupMetaData(Result result)1042         private void loadGroupMetaData(Result result) {
1043             StringBuilder selection = new StringBuilder();
1044             ArrayList<String> selectionArgs = new ArrayList<String>();
1045             for (Entity entity : result.mEntities) {
1046                 ContentValues values = entity.getEntityValues();
1047                 String accountName = values.getAsString(RawContacts.ACCOUNT_NAME);
1048                 String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
1049                 String dataSet = values.getAsString(RawContacts.DATA_SET);
1050                 if (accountName != null && accountType != null) {
1051                     if (selection.length() != 0) {
1052                         selection.append(" OR ");
1053                     }
1054                     selection.append(
1055                             "(" + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?");
1056                     selectionArgs.add(accountName);
1057                     selectionArgs.add(accountType);
1058 
1059                     if (dataSet != null) {
1060                         selection.append(" AND " + Groups.DATA_SET + "=?");
1061                         selectionArgs.add(dataSet);
1062                     } else {
1063                         selection.append(" AND " + Groups.DATA_SET + " IS NULL");
1064                     }
1065                     selection.append(")");
1066                 }
1067             }
1068             Cursor cursor = getContext().getContentResolver().query(Groups.CONTENT_URI,
1069                     GroupQuery.COLUMNS, selection.toString(), selectionArgs.toArray(new String[0]),
1070                     null);
1071             try {
1072                 while (cursor.moveToNext()) {
1073                     final String accountName = cursor.getString(GroupQuery.ACCOUNT_NAME);
1074                     final String accountType = cursor.getString(GroupQuery.ACCOUNT_TYPE);
1075                     final String dataSet = cursor.getString(GroupQuery.DATA_SET);
1076                     final long groupId = cursor.getLong(GroupQuery.ID);
1077                     final String title = cursor.getString(GroupQuery.TITLE);
1078                     final boolean defaultGroup = cursor.isNull(GroupQuery.AUTO_ADD)
1079                             ? false
1080                             : cursor.getInt(GroupQuery.AUTO_ADD) != 0;
1081                     final boolean favorites = cursor.isNull(GroupQuery.FAVORITES)
1082                             ? false
1083                             : cursor.getInt(GroupQuery.FAVORITES) != 0;
1084 
1085                     result.addGroupMetaData(new GroupMetaData(
1086                             accountName, accountType, dataSet, groupId, title, defaultGroup,
1087                             favorites));
1088                 }
1089             } finally {
1090                 cursor.close();
1091             }
1092         }
1093 
1094         /**
1095          * Loads all stream items and stream item photos belonging to this contact.
1096          */
loadStreamItems(Result result)1097         private void loadStreamItems(Result result) {
1098             Cursor cursor = getContext().getContentResolver().query(
1099                     Contacts.CONTENT_LOOKUP_URI.buildUpon()
1100                             .appendPath(result.getLookupKey())
1101                             .appendPath(Contacts.StreamItems.CONTENT_DIRECTORY).build(),
1102                     null, null, null, null);
1103             Map<Long, StreamItemEntry> streamItemsById = new HashMap<Long, StreamItemEntry>();
1104             ArrayList<StreamItemEntry> streamItems = new ArrayList<StreamItemEntry>();
1105             try {
1106                 while (cursor.moveToNext()) {
1107                     StreamItemEntry streamItem = new StreamItemEntry(cursor);
1108                     streamItemsById.put(streamItem.getId(), streamItem);
1109                     streamItems.add(streamItem);
1110                 }
1111             } finally {
1112                 cursor.close();
1113             }
1114 
1115             // Now retrieve any photo records associated with the stream items.
1116             if (!streamItems.isEmpty()) {
1117                 if (result.isUserProfile()) {
1118                     // If the stream items we're loading are for the profile, we can't bulk-load the
1119                     // stream items with a custom selection.
1120                     for (StreamItemEntry entry : streamItems) {
1121                         Cursor siCursor = getContext().getContentResolver().query(
1122                                 Uri.withAppendedPath(
1123                                         ContentUris.withAppendedId(
1124                                                 StreamItems.CONTENT_URI, entry.getId()),
1125                                         StreamItems.StreamItemPhotos.CONTENT_DIRECTORY),
1126                                 null, null, null, null);
1127                         try {
1128                             while (siCursor.moveToNext()) {
1129                                 entry.addPhoto(new StreamItemPhotoEntry(siCursor));
1130                             }
1131                         } finally {
1132                             siCursor.close();
1133                         }
1134                     }
1135                 } else {
1136                     String[] streamItemIdArr = new String[streamItems.size()];
1137                     StringBuilder streamItemPhotoSelection = new StringBuilder();
1138                     streamItemPhotoSelection.append(StreamItemPhotos.STREAM_ITEM_ID + " IN (");
1139                     for (int i = 0; i < streamItems.size(); i++) {
1140                         if (i > 0) {
1141                             streamItemPhotoSelection.append(",");
1142                         }
1143                         streamItemPhotoSelection.append("?");
1144                         streamItemIdArr[i] = String.valueOf(streamItems.get(i).getId());
1145                     }
1146                     streamItemPhotoSelection.append(")");
1147                     Cursor sipCursor = getContext().getContentResolver().query(
1148                             StreamItems.CONTENT_PHOTO_URI,
1149                             null, streamItemPhotoSelection.toString(), streamItemIdArr,
1150                             StreamItemPhotos.STREAM_ITEM_ID);
1151                     try {
1152                         while (sipCursor.moveToNext()) {
1153                             long streamItemId = sipCursor.getLong(
1154                                     sipCursor.getColumnIndex(StreamItemPhotos.STREAM_ITEM_ID));
1155                             StreamItemEntry streamItem = streamItemsById.get(streamItemId);
1156                             streamItem.addPhoto(new StreamItemPhotoEntry(sipCursor));
1157                         }
1158                     } finally {
1159                         sipCursor.close();
1160                     }
1161                 }
1162             }
1163 
1164             // Set the sorted stream items on the result.
1165             Collections.sort(streamItems);
1166             result.mStreamItems.addAll(streamItems);
1167         }
1168 
1169         @Override
onPostExecute(Result result)1170         protected void onPostExecute(Result result) {
1171             unregisterObserver();
1172 
1173             // The creator isn't interested in any further updates
1174             if (mDestroyed || result == null) {
1175                 return;
1176             }
1177 
1178             mContact = result;
1179 
1180             if (result.isLoaded()) {
1181                 mLookupUri = result.getLookupUri();
1182 
1183                 if (!result.isDirectoryEntry()) {
1184                     Log.i(TAG, "Registering content observer for " + mLookupUri);
1185                     if (mObserver == null) {
1186                         mObserver = new ForceLoadContentObserver();
1187                     }
1188                     getContext().getContentResolver().registerContentObserver(
1189                             mLookupUri, true, mObserver);
1190                 }
1191 
1192                 if (mContact.getPhotoBinaryData() == null && mContact.getPhotoUri() != null) {
1193                     mContact.setLoadingPhoto(true);
1194                     new AsyncPhotoLoader().execute(mContact.getPhotoUri());
1195                 }
1196 
1197                 // inform the source of the data that this contact is being looked at
1198                 postViewNotificationToSyncAdapter();
1199             }
1200 
1201             deliverResult(mContact);
1202         }
1203     }
1204 
1205     /**
1206      * Posts a message to the contributing sync adapters that have opted-in, notifying them
1207      * that the contact has just been loaded
1208      */
postViewNotificationToSyncAdapter()1209     private void postViewNotificationToSyncAdapter() {
1210         Context context = getContext();
1211         for (Entity entity : mContact.getEntities()) {
1212             final ContentValues entityValues = entity.getEntityValues();
1213             final long rawContactId = entityValues.getAsLong(RawContacts.Entity._ID);
1214             if (mNotifiedRawContactIds.contains(rawContactId)) {
1215                 continue; // Already notified for this raw contact.
1216             }
1217             mNotifiedRawContactIds.add(rawContactId);
1218             final String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
1219             final String dataSet = entityValues.getAsString(RawContacts.DATA_SET);
1220             final AccountType accountType = AccountTypeManager.getInstance(context).getAccountType(
1221                     type, dataSet);
1222             final String serviceName = accountType.getViewContactNotifyServiceClassName();
1223             final String resPackageName = accountType.resPackageName;
1224             if (!TextUtils.isEmpty(serviceName) && !TextUtils.isEmpty(resPackageName)) {
1225                 final Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
1226                 final Intent intent = new Intent();
1227                 intent.setClassName(resPackageName, serviceName);
1228                 intent.setAction(Intent.ACTION_VIEW);
1229                 intent.setDataAndType(uri, RawContacts.CONTENT_ITEM_TYPE);
1230                 try {
1231                     context.startService(intent);
1232                 } catch (Exception e) {
1233                     Log.e(TAG, "Error sending message to source-app", e);
1234                 }
1235             }
1236         }
1237     }
1238 
1239     private class AsyncPhotoLoader extends AsyncTask<String, Void, byte[]> {
1240 
1241         private static final int BUFFER_SIZE = 1024*16;
1242 
1243         @Override
doInBackground(String... params)1244         protected byte[] doInBackground(String... params) {
1245             Uri uri = Uri.parse(params[0]);
1246             byte[] data = null;
1247             try {
1248                 InputStream is = getContext().getContentResolver().openInputStream(uri);
1249                 if (is != null) {
1250                     ByteArrayOutputStream baos = new ByteArrayOutputStream();
1251                     try {
1252                         byte[] mBuffer = new byte[BUFFER_SIZE];
1253 
1254                         int size;
1255                         while ((size = is.read(mBuffer)) != -1) {
1256                             baos.write(mBuffer, 0, size);
1257                         }
1258                         data = baos.toByteArray();
1259                     } finally {
1260                         is.close();
1261                     }
1262                 } else {
1263                     Log.v(TAG, "Cannot load photo " + uri);
1264                 }
1265             } catch (IOException e) {
1266                 Log.e(TAG, "Cannot load photo " + uri, e);
1267             }
1268 
1269             return data;
1270         }
1271 
1272         @Override
onPostExecute(byte[] data)1273         protected void onPostExecute(byte[] data) {
1274             if (mContact != null) {
1275                 mContact = new Result(mContact);
1276                 mContact.setPhotoBinaryData(data);
1277                 mContact.setLoadingPhoto(false);
1278                 deliverResult(mContact);
1279             }
1280         }
1281     }
1282 
unregisterObserver()1283     private void unregisterObserver() {
1284         if (mObserver != null) {
1285             getContext().getContentResolver().unregisterContentObserver(mObserver);
1286             mObserver = null;
1287         }
1288     }
1289 
ContactLoader(Context context, Uri lookupUri)1290     public ContactLoader(Context context, Uri lookupUri) {
1291         this(context, lookupUri, false, false, false);
1292     }
1293 
ContactLoader(Context context, Uri lookupUri, boolean loadGroupMetaData, boolean loadStreamItems, boolean loadInvitableAccountTypes)1294     public ContactLoader(Context context, Uri lookupUri, boolean loadGroupMetaData,
1295             boolean loadStreamItems, boolean loadInvitableAccountTypes) {
1296         super(context);
1297         mLookupUri = lookupUri;
1298         mRequestedUri = lookupUri;
1299         mLoadGroupMetaData = loadGroupMetaData;
1300         mLoadStreamItems = loadStreamItems;
1301         mLoadInvitableAccountTypes = loadInvitableAccountTypes;
1302     }
1303 
getLookupUri()1304     public Uri getLookupUri() {
1305         return mLookupUri;
1306     }
1307 
1308     @Override
onStartLoading()1309     protected void onStartLoading() {
1310         if (mContact != null) {
1311             deliverResult(mContact);
1312         }
1313 
1314         if (takeContentChanged() || mContact == null) {
1315             forceLoad();
1316         }
1317     }
1318 
1319     @Override
onForceLoad()1320     protected void onForceLoad() {
1321         final LoadContactTask task = new LoadContactTask();
1322         task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[])null);
1323     }
1324 
1325     @Override
onReset()1326     protected void onReset() {
1327         unregisterObserver();
1328         mContact = null;
1329         mDestroyed = true;
1330     }
1331 }
1332