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