ContactLoader.java revision 08bcf715d5ea7f07ce18a282d9850ac70552ca9d
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.DataStatus;
23import com.android.contacts.util.StreamItemEntry;
24import com.android.contacts.util.StreamItemPhotoEntry;
25import com.google.android.collect.Lists;
26import com.google.common.annotations.VisibleForTesting;
27import com.google.common.collect.Maps;
28import com.google.common.collect.Sets;
29
30import android.content.ContentResolver;
31import android.content.ContentUris;
32import android.content.ContentValues;
33import android.content.Context;
34import android.content.Entity;
35import android.content.Entity.NamedContentValues;
36import android.content.Intent;
37import android.content.Loader;
38import android.content.pm.PackageManager;
39import android.content.pm.PackageManager.NameNotFoundException;
40import android.content.res.AssetFileDescriptor;
41import android.content.res.Resources;
42import android.database.Cursor;
43import android.net.Uri;
44import android.os.AsyncTask;
45import android.provider.ContactsContract;
46import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
47import android.provider.ContactsContract.CommonDataKinds.Photo;
48import android.provider.ContactsContract.Contacts;
49import android.provider.ContactsContract.Data;
50import android.provider.ContactsContract.Directory;
51import android.provider.ContactsContract.DisplayNameSources;
52import android.provider.ContactsContract.Groups;
53import android.provider.ContactsContract.RawContacts;
54import android.provider.ContactsContract.StreamItemPhotos;
55import android.provider.ContactsContract.StreamItems;
56import android.text.TextUtils;
57import android.util.Log;
58
59import java.io.ByteArrayOutputStream;
60import java.io.FileInputStream;
61import java.io.IOException;
62import java.io.InputStream;
63import java.util.ArrayList;
64import java.util.Collections;
65import java.util.HashMap;
66import java.util.List;
67import java.util.Map;
68import java.util.Set;
69
70/**
71 * Loads a single Contact and all it constituent RawContacts.
72 */
73public 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 {
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         */
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
173        private static Result forError(Uri requestedUri, Exception exception) {
174            return new Result(requestedUri, Status.ERROR, exception);
175        }
176
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         */
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
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         */
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
265        private void setLoadingPhoto(boolean flag) {
266            mLoadingPhoto = flag;
267        }
268
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         */
278        public Uri getLookupUri() {
279            return mLookupUri;
280        }
281
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         */
292        public Uri getUri() {
293            return mUri;
294        }
295
296        /**
297         * Returns the URI for which this {@link ContactLoader) was initially requested.
298         */
299        public Uri getRequestedUri() {
300            return mRequestedUri;
301        }
302
303        @VisibleForTesting
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         */
315        public boolean isError() {
316            return mStatus == Status.ERROR;
317        }
318
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         */
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         */
337        public boolean isLoaded() {
338            return mStatus == Status.LOADED;
339        }
340
341        public long getNameRawContactId() {
342            return mNameRawContactId;
343        }
344
345        public int getDisplayNameSource() {
346            return mDisplayNameSource;
347        }
348
349        public long getPhotoId() {
350            return mPhotoId;
351        }
352
353        public String getPhotoUri() {
354            return mPhotoUri;
355        }
356
357        public String getDisplayName() {
358            return mDisplayName;
359        }
360
361        public String getAltDisplayName() {
362            return mAltDisplayName;
363        }
364
365        public String getPhoneticName() {
366            return mPhoneticName;
367        }
368
369        public boolean getStarred() {
370            return mStarred;
371        }
372
373        public Integer getPresence() {
374            return mPresence;
375        }
376
377        public ArrayList<AccountType> getInvitableAccountTypes() {
378            return mInvitableAccountTypes;
379        }
380
381        public ArrayList<Entity> getEntities() {
382            return mEntities;
383        }
384
385        public ArrayList<StreamItemEntry> getStreamItems() {
386            return mStreamItems;
387        }
388
389        public HashMap<Long, DataStatus> getStatuses() {
390            return mStatuses;
391        }
392
393        public long getDirectoryId() {
394            return mDirectoryId;
395        }
396
397        public boolean isDirectoryEntry() {
398            return mDirectoryId != -1 && mDirectoryId != Directory.DEFAULT
399                    && mDirectoryId != Directory.LOCAL_INVISIBLE;
400        }
401
402        public int getDirectoryExportSupport() {
403            return mDirectoryExportSupport;
404        }
405
406        public String getDirectoryDisplayName() {
407            return mDirectoryDisplayName;
408        }
409
410        public String getDirectoryType() {
411            return mDirectoryType;
412        }
413
414        public String getDirectoryAccountType() {
415            return mDirectoryAccountType;
416        }
417
418        public String getDirectoryAccountName() {
419            return mDirectoryAccountName;
420        }
421
422        public boolean isLoadingPhoto() {
423            return mLoadingPhoto;
424        }
425
426        public byte[] getPhotoBinaryData() {
427            return mPhotoBinaryData;
428        }
429
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
461        private void addGroupMetaData(GroupMetaData group) {
462            if (mGroups == null) {
463                mGroups = new ArrayList<GroupMetaData>();
464            }
465            mGroups.add(group);
466        }
467
468        public List<GroupMetaData> getGroupMetaData() {
469            return mGroups;
470        }
471
472        public boolean isSendToVoicemail() {
473            return mSendToVoicemail;
474        }
475
476        public String getCustomRingtone() {
477            return mCustomRingtone;
478        }
479
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
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         */
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
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         */
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         */
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         */
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         */
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         */
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
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
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         */
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         */
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
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     */
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
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
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
1283    private void unregisterObserver() {
1284        if (mObserver != null) {
1285            getContext().getContentResolver().unregisterContentObserver(mObserver);
1286            mObserver = null;
1287        }
1288    }
1289
1290    public ContactLoader(Context context, Uri lookupUri) {
1291        this(context, lookupUri, false, false, false);
1292    }
1293
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
1304    public Uri getLookupUri() {
1305        return mLookupUri;
1306    }
1307
1308    @Override
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
1320    protected void onForceLoad() {
1321        final LoadContactTask task = new LoadContactTask();
1322        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[])null);
1323    }
1324
1325    @Override
1326    protected void onReset() {
1327        unregisterObserver();
1328        mContact = null;
1329        mDestroyed = true;
1330    }
1331}
1332