ContactLoader.java revision 97e90c6d0938e31c2af4ab4b6b055cda853502c5
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.util.DataStatus;
20
21import android.content.ContentResolver;
22import android.content.ContentUris;
23import android.content.ContentValues;
24import android.content.Context;
25import android.content.Entity;
26import android.content.Entity.NamedContentValues;
27import android.content.Loader;
28import android.content.pm.PackageManager;
29import android.content.pm.PackageManager.NameNotFoundException;
30import android.content.res.Resources;
31import android.database.Cursor;
32import android.net.Uri;
33import android.os.AsyncTask;
34import android.provider.ContactsContract;
35import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
36import android.provider.ContactsContract.CommonDataKinds.Photo;
37import android.provider.ContactsContract.Contacts;
38import android.provider.ContactsContract.Data;
39import android.provider.ContactsContract.Directory;
40import android.provider.ContactsContract.DisplayNameSources;
41import android.provider.ContactsContract.Groups;
42import android.provider.ContactsContract.RawContacts;
43import android.text.TextUtils;
44import android.util.Log;
45
46import java.io.ByteArrayOutputStream;
47import java.io.IOException;
48import java.io.InputStream;
49import java.util.ArrayList;
50import java.util.HashMap;
51import java.util.List;
52
53/**
54 * Loads a single Contact and all it constituent RawContacts.
55 */
56public class ContactLoader extends Loader<ContactLoader.Result> {
57    private static final String TAG = "ContactLoader";
58
59    private Uri mLookupUri;
60    private boolean mLoadGroupMetaData;
61    private Result mContact;
62    private ForceLoadContentObserver mObserver;
63    private boolean mDestroyed;
64
65
66    public interface Listener {
67        public void onContactLoaded(Result contact);
68    }
69
70    /**
71     * The result of a load operation. Contains all data necessary to display the contact.
72     */
73    public static final class Result {
74        /**
75         * Singleton instance that represents "No Contact Found"
76         */
77        public static final Result NOT_FOUND = new Result();
78
79        /**
80         * Singleton instance that represents an error, e.g. because of an invalid Uri
81         * TODO: We should come up with something nicer here. Maybe use an Either type so
82         * that we can capture the Exception?
83         */
84        public static final Result ERROR = new Result();
85
86        private final Uri mLookupUri;
87        private final Uri mUri;
88        private final long mDirectoryId;
89        private final String mLookupKey;
90        private final long mId;
91        private final long mNameRawContactId;
92        private final int mDisplayNameSource;
93        private final long mPhotoId;
94        private final String mPhotoUri;
95        private final String mDisplayName;
96        private final String mPhoneticName;
97        private final boolean mStarred;
98        private final Integer mPresence;
99        private final ArrayList<Entity> mEntities;
100        private final HashMap<Long, DataStatus> mStatuses;
101        private final String mStatus;
102        private final Long mStatusTimestamp;
103        private final Integer mStatusLabel;
104        private final String mStatusResPackage;
105
106        private String mDirectoryDisplayName;
107        private String mDirectoryType;
108        private String mDirectoryAccountType;
109        private String mDirectoryAccountName;
110        private int mDirectoryExportSupport;
111
112        private ArrayList<GroupMetaData> mGroups;
113
114        private boolean mLoadingPhoto;
115        private byte[] mPhotoBinaryData;
116
117        /**
118         * Constructor for case "no contact found". This must only be used for the
119         * final {@link Result#NOT_FOUND} singleton
120         */
121        private Result() {
122            mLookupUri = null;
123            mUri = null;
124            mDirectoryId = -1;
125            mLookupKey = null;
126            mId = -1;
127            mEntities = null;
128            mStatuses = null;
129            mNameRawContactId = -1;
130            mDisplayNameSource = DisplayNameSources.UNDEFINED;
131            mPhotoId = -1;
132            mPhotoUri = null;
133            mDisplayName = null;
134            mPhoneticName = null;
135            mStarred = false;
136            mPresence = null;
137            mStatus = null;
138            mStatusTimestamp = null;
139            mStatusLabel = null;
140            mStatusResPackage = null;
141        }
142
143        /**
144         * Constructor to call when contact was found
145         */
146        private Result(Uri uri, Uri lookupUri, long directoryId, String lookupKey, long id,
147                long nameRawContactId, int displayNameSource, long photoId, String photoUri,
148                String displayName, String phoneticName, boolean starred, Integer presence,
149                String status, Long statusTimestamp, Integer statusLabel, String statusResPackage) {
150            mLookupUri = lookupUri;
151            mUri = uri;
152            mDirectoryId = directoryId;
153            mLookupKey = lookupKey;
154            mId = id;
155            mEntities = new ArrayList<Entity>();
156            mStatuses = new HashMap<Long, DataStatus>();
157            mNameRawContactId = nameRawContactId;
158            mDisplayNameSource = displayNameSource;
159            mPhotoId = photoId;
160            mPhotoUri = photoUri;
161            mDisplayName = displayName;
162            mPhoneticName = phoneticName;
163            mStarred = starred;
164            mPresence = presence;
165            mStatus = status;
166            mStatusTimestamp = statusTimestamp;
167            mStatusLabel = statusLabel;
168            mStatusResPackage = statusResPackage;
169        }
170
171        /**
172         * @param exportSupport See {@link Directory#EXPORT_SUPPORT}.
173         */
174        private void setDirectoryMetaData(String displayName, String directoryType,
175                String accountType, String accountName, int exportSupport) {
176            mDirectoryDisplayName = displayName;
177            mDirectoryType = directoryType;
178            mDirectoryAccountType = accountType;
179            mDirectoryAccountName = accountName;
180            mDirectoryExportSupport = exportSupport;
181        }
182
183        private void setLoadingPhoto(boolean flag) {
184            mLoadingPhoto = flag;
185        }
186
187        private void setPhotoBinaryData(byte[] photoBinaryData) {
188            mPhotoBinaryData = photoBinaryData;
189        }
190
191        public Uri getLookupUri() {
192            return mLookupUri;
193        }
194
195        public String getLookupKey() {
196            return mLookupKey;
197        }
198
199        public Uri getUri() {
200            return mUri;
201        }
202
203        public long getId() {
204            return mId;
205        }
206
207        public long getNameRawContactId() {
208            return mNameRawContactId;
209        }
210
211        public int getDisplayNameSource() {
212            return mDisplayNameSource;
213        }
214
215        public long getPhotoId() {
216            return mPhotoId;
217        }
218
219        public String getPhotoUri() {
220            return mPhotoUri;
221        }
222
223        public String getDisplayName() {
224            return mDisplayName;
225        }
226
227        public String getPhoneticName() {
228            return mPhoneticName;
229        }
230
231        public boolean getStarred() {
232            return mStarred;
233        }
234
235        public Integer getPresence() {
236            return mPresence;
237        }
238
239        public String getSocialSnippet() {
240            return mStatus;
241        }
242
243        public Long getStatusTimestamp() {
244            return mStatusTimestamp;
245        }
246
247        public Integer getStatusLabel() {
248            return mStatusLabel;
249        }
250
251        public String getStatusResPackage() {
252            return mStatusResPackage;
253        }
254
255        public ArrayList<Entity> getEntities() {
256            return mEntities;
257        }
258
259        public HashMap<Long, DataStatus> getStatuses() {
260            return mStatuses;
261        }
262
263        public long getDirectoryId() {
264            return mDirectoryId;
265        }
266
267        public boolean isDirectoryEntry() {
268            return mDirectoryId != -1 && mDirectoryId != Directory.DEFAULT
269                    && mDirectoryId != Directory.LOCAL_INVISIBLE;
270        }
271
272        public int getDirectoryExportSupport() {
273            return mDirectoryExportSupport;
274        }
275
276        public String getDirectoryDisplayName() {
277            return mDirectoryDisplayName;
278        }
279
280        public String getDirectoryType() {
281            return mDirectoryType;
282        }
283
284        public String getDirectoryAccountType() {
285            return mDirectoryAccountType;
286        }
287
288        public String getDirectoryAccountName() {
289            return mDirectoryAccountName;
290        }
291
292        public boolean isLoadingPhoto() {
293            return mLoadingPhoto;
294        }
295
296        public byte[] getPhotoBinaryData() {
297            return mPhotoBinaryData;
298        }
299
300        public ArrayList<ContentValues> getContentValues() {
301            if (mEntities.size() != 1) {
302                throw new IllegalStateException(
303                        "Cannot extract content values from an aggregated contact");
304            }
305
306            Entity entity = mEntities.get(0);
307            ArrayList<ContentValues> result = new ArrayList<ContentValues>();
308            ArrayList<NamedContentValues> subValues = entity.getSubValues();
309            if (subValues != null) {
310                int size = subValues.size();
311                for (int i = 0; i < size; i++) {
312                    NamedContentValues pair = subValues.get(i);
313                    if (Data.CONTENT_URI.equals(pair.uri)) {
314                        result.add(pair.values);
315                    }
316                }
317            }
318
319            // If the photo was loaded using the URI, create an entry for the photo
320            // binary data.
321            if (mPhotoId == 0 && mPhotoBinaryData != null) {
322                ContentValues photo = new ContentValues();
323                photo.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
324                photo.put(Photo.PHOTO, mPhotoBinaryData);
325                result.add(photo);
326            }
327
328            return result;
329        }
330
331        private void addGroupMetaData(GroupMetaData group) {
332            if (mGroups == null) {
333                mGroups = new ArrayList<GroupMetaData>();
334            }
335            mGroups.add(group);
336        }
337
338        public List<GroupMetaData> getGroupMetaData() {
339            return mGroups;
340        }
341    }
342
343    private static class ContactQuery {
344        // Projection used for the query that loads all data for the entire contact.
345        final static String[] COLUMNS = new String[] {
346                Contacts.NAME_RAW_CONTACT_ID,
347                Contacts.DISPLAY_NAME_SOURCE,
348                Contacts.LOOKUP_KEY,
349                Contacts.DISPLAY_NAME,
350                Contacts.PHONETIC_NAME,
351                Contacts.PHOTO_ID,
352                Contacts.STARRED,
353                Contacts.CONTACT_PRESENCE,
354                Contacts.CONTACT_STATUS,
355                Contacts.CONTACT_STATUS_TIMESTAMP,
356                Contacts.CONTACT_STATUS_RES_PACKAGE,
357                Contacts.CONTACT_STATUS_LABEL,
358                Contacts.Entity.CONTACT_ID,
359                Contacts.Entity.RAW_CONTACT_ID,
360
361                RawContacts.ACCOUNT_NAME,
362                RawContacts.ACCOUNT_TYPE,
363                RawContacts.DIRTY,
364                RawContacts.VERSION,
365                RawContacts.SOURCE_ID,
366                RawContacts.SYNC1,
367                RawContacts.SYNC2,
368                RawContacts.SYNC3,
369                RawContacts.SYNC4,
370                RawContacts.DELETED,
371                RawContacts.IS_RESTRICTED,
372                RawContacts.NAME_VERIFIED,
373
374                Contacts.Entity.DATA_ID,
375                Data.DATA1,
376                Data.DATA2,
377                Data.DATA3,
378                Data.DATA4,
379                Data.DATA5,
380                Data.DATA6,
381                Data.DATA7,
382                Data.DATA8,
383                Data.DATA9,
384                Data.DATA10,
385                Data.DATA11,
386                Data.DATA12,
387                Data.DATA13,
388                Data.DATA14,
389                Data.DATA15,
390                Data.SYNC1,
391                Data.SYNC2,
392                Data.SYNC3,
393                Data.SYNC4,
394                Data.DATA_VERSION,
395                Data.IS_PRIMARY,
396                Data.IS_SUPER_PRIMARY,
397                Data.MIMETYPE,
398                Data.RES_PACKAGE,
399
400                GroupMembership.GROUP_SOURCE_ID,
401
402                Data.PRESENCE,
403                Data.CHAT_CAPABILITY,
404                Data.STATUS,
405                Data.STATUS_RES_PACKAGE,
406                Data.STATUS_ICON,
407                Data.STATUS_LABEL,
408                Data.STATUS_TIMESTAMP,
409
410                Contacts.PHOTO_URI,
411        };
412
413        public final static int NAME_RAW_CONTACT_ID = 0;
414        public final static int DISPLAY_NAME_SOURCE = 1;
415        public final static int LOOKUP_KEY = 2;
416        public final static int DISPLAY_NAME = 3;
417        public final static int PHONETIC_NAME = 4;
418        public final static int PHOTO_ID = 5;
419        public final static int STARRED = 6;
420        public final static int CONTACT_PRESENCE = 7;
421        public final static int CONTACT_STATUS = 8;
422        public final static int CONTACT_STATUS_TIMESTAMP = 9;
423        public final static int CONTACT_STATUS_RES_PACKAGE = 10;
424        public final static int CONTACT_STATUS_LABEL = 11;
425        public final static int CONTACT_ID = 12;
426        public final static int RAW_CONTACT_ID = 13;
427
428        public final static int ACCOUNT_NAME = 14;
429        public final static int ACCOUNT_TYPE = 15;
430        public final static int DIRTY = 16;
431        public final static int VERSION = 17;
432        public final static int SOURCE_ID = 18;
433        public final static int SYNC1 = 19;
434        public final static int SYNC2 = 20;
435        public final static int SYNC3 = 21;
436        public final static int SYNC4 = 22;
437        public final static int DELETED = 23;
438        public final static int IS_RESTRICTED = 24;
439        public final static int NAME_VERIFIED = 25;
440
441        public final static int DATA_ID = 26;
442        public final static int DATA1 = 27;
443        public final static int DATA2 = 28;
444        public final static int DATA3 = 29;
445        public final static int DATA4 = 30;
446        public final static int DATA5 = 31;
447        public final static int DATA6 = 32;
448        public final static int DATA7 = 33;
449        public final static int DATA8 = 34;
450        public final static int DATA9 = 35;
451        public final static int DATA10 = 36;
452        public final static int DATA11 = 37;
453        public final static int DATA12 = 38;
454        public final static int DATA13 = 39;
455        public final static int DATA14 = 40;
456        public final static int DATA15 = 41;
457        public final static int DATA_SYNC1 = 42;
458        public final static int DATA_SYNC2 = 43;
459        public final static int DATA_SYNC3 = 44;
460        public final static int DATA_SYNC4 = 45;
461        public final static int DATA_VERSION = 46;
462        public final static int IS_PRIMARY = 47;
463        public final static int IS_SUPERPRIMARY = 48;
464        public final static int MIMETYPE = 49;
465        public final static int RES_PACKAGE = 50;
466
467        public final static int GROUP_SOURCE_ID = 51;
468
469        public final static int PRESENCE = 52;
470        public final static int CHAT_CAPABILITY = 53;
471        public final static int STATUS = 54;
472        public final static int STATUS_RES_PACKAGE = 55;
473        public final static int STATUS_ICON = 56;
474        public final static int STATUS_LABEL = 57;
475        public final static int STATUS_TIMESTAMP = 58;
476
477        public final static int PHOTO_URI = 59;
478    }
479
480    private static class DirectoryQuery {
481        // Projection used for the query that loads all data for the entire contact.
482        final static String[] COLUMNS = new String[] {
483            Directory.DISPLAY_NAME,
484            Directory.PACKAGE_NAME,
485            Directory.TYPE_RESOURCE_ID,
486            Directory.ACCOUNT_TYPE,
487            Directory.ACCOUNT_NAME,
488            Directory.EXPORT_SUPPORT,
489        };
490
491        public final static int DISPLAY_NAME = 0;
492        public final static int PACKAGE_NAME = 1;
493        public final static int TYPE_RESOURCE_ID = 2;
494        public final static int ACCOUNT_TYPE = 3;
495        public final static int ACCOUNT_NAME = 4;
496        public final static int EXPORT_SUPPORT = 5;
497    }
498
499    private static class GroupQuery {
500        final static String[] COLUMNS = new String[] {
501            Groups.ACCOUNT_NAME,
502            Groups.ACCOUNT_TYPE,
503            Groups._ID,
504            Groups.TITLE,
505            Groups.AUTO_ADD,
506            Groups.FAVORITES,
507        };
508
509        public final static int ACCOUNT_NAME = 0;
510        public final static int ACCOUNT_TYPE = 1;
511        public final static int ID = 2;
512        public final static int TITLE = 3;
513        public final static int AUTO_ADD = 4;
514        public final static int FAVORITES = 5;
515    }
516
517    private final class LoadContactTask extends AsyncTask<Void, Void, Result> {
518
519        @Override
520        protected Result doInBackground(Void... args) {
521            try {
522                final ContentResolver resolver = getContext().getContentResolver();
523                final Uri uriCurrentFormat = ensureIsContactUri(resolver, mLookupUri);
524                Result result = loadContactEntity(resolver, uriCurrentFormat);
525                if (result != Result.NOT_FOUND) {
526                    if (result.isDirectoryEntry()) {
527                        loadDirectoryMetaData(result);
528                    } else if (mLoadGroupMetaData) {
529                        loadGroupMetaData(result);
530                    }
531                    loadPhotoBinaryData(result);
532                }
533                return result;
534            } catch (Exception e) {
535                Log.e(TAG, "Error loading the contact: " + mLookupUri, e);
536                return Result.ERROR;
537            }
538        }
539
540        /**
541         * Transforms the given Uri and returns a Lookup-Uri that represents the contact.
542         * For legacy contacts, a raw-contact lookup is performed.
543         * @param resolver
544         */
545        private Uri ensureIsContactUri(final ContentResolver resolver, final Uri uri) {
546            if (uri == null) throw new IllegalArgumentException("uri must not be null");
547
548            final String authority = uri.getAuthority();
549
550            // Current Style Uri?
551            if (ContactsContract.AUTHORITY.equals(authority)) {
552                final String type = resolver.getType(uri);
553                // Contact-Uri? Good, return it
554                if (Contacts.CONTENT_ITEM_TYPE.equals(type)) {
555                    return uri;
556                }
557
558                // RawContact-Uri? Transform it to ContactUri
559                if (RawContacts.CONTENT_ITEM_TYPE.equals(type)) {
560                    final long rawContactId = ContentUris.parseId(uri);
561                    return RawContacts.getContactLookupUri(getContext().getContentResolver(),
562                            ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
563                }
564
565                // Anything else? We don't know what this is
566                throw new IllegalArgumentException("uri format is unknown");
567            }
568
569            // Legacy Style? Convert to RawContact
570            final String OBSOLETE_AUTHORITY = "contacts";
571            if (OBSOLETE_AUTHORITY.equals(authority)) {
572                // Legacy Format. Convert to RawContact-Uri and then lookup the contact
573                final long rawContactId = ContentUris.parseId(uri);
574                return RawContacts.getContactLookupUri(resolver,
575                        ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
576            }
577
578            throw new IllegalArgumentException("uri authority is unknown");
579        }
580
581        private Result loadContactEntity(ContentResolver resolver, Uri contactUri) {
582            Uri entityUri = Uri.withAppendedPath(contactUri, Contacts.Entity.CONTENT_DIRECTORY);
583            Cursor cursor = resolver.query(entityUri, ContactQuery.COLUMNS, null, null,
584                    Contacts.Entity.RAW_CONTACT_ID);
585            if (cursor == null) {
586                Log.e(TAG, "No cursor returned in loadContactEntity");
587                return Result.NOT_FOUND;
588            }
589
590            try {
591                if (!cursor.moveToFirst()) {
592                    cursor.close();
593                    return Result.NOT_FOUND;
594                }
595
596                long currentRawContactId = -1;
597                Entity entity = null;
598                Result result = loadContactHeaderData(cursor, contactUri);
599                ArrayList<Entity> entities = result.getEntities();
600                HashMap<Long, DataStatus> statuses = result.getStatuses();
601                for (; !cursor.isAfterLast(); cursor.moveToNext()) {
602                    long rawContactId = cursor.getLong(ContactQuery.RAW_CONTACT_ID);
603                    if (rawContactId != currentRawContactId) {
604                        currentRawContactId = rawContactId;
605                        entity = new android.content.Entity(loadRawContact(cursor));
606                        entities.add(entity);
607                    }
608                    if (!cursor.isNull(ContactQuery.DATA_ID)) {
609                        ContentValues data = loadData(cursor);
610                        entity.addSubValue(ContactsContract.Data.CONTENT_URI, data);
611
612                        if (!cursor.isNull(ContactQuery.PRESENCE)
613                                || !cursor.isNull(ContactQuery.STATUS)) {
614                            final DataStatus status = new DataStatus(cursor);
615                            final long dataId = cursor.getLong(ContactQuery.DATA_ID);
616                            statuses.put(dataId, status);
617                        }
618                    }
619                }
620
621                return result;
622            } finally {
623                cursor.close();
624            }
625        }
626
627        /**
628         * Looks for the photo data item in entities. If found, creates a new Bitmap instance. If
629         * not found, returns null
630         */
631        private void loadPhotoBinaryData(Result contactData) {
632            final long photoId = contactData.getPhotoId();
633            if (photoId <= 0) {
634                // No photo ID
635                return;
636            }
637
638            for (Entity entity : contactData.getEntities()) {
639                for (NamedContentValues subValue : entity.getSubValues()) {
640                    final ContentValues entryValues = subValue.values;
641                    final long dataId = entryValues.getAsLong(Data._ID);
642                    if (dataId == photoId) {
643                        final String mimeType = entryValues.getAsString(Data.MIMETYPE);
644                        // Correct Data Id but incorrect MimeType? Don't load
645                        if (!Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
646                            return;
647                        }
648                        contactData.setPhotoBinaryData(entryValues.getAsByteArray(Photo.PHOTO));
649                        break;
650                    }
651                }
652            }
653        }
654
655        /**
656         * Extracts Contact level columns from the cursor.
657         */
658        private Result loadContactHeaderData(final Cursor cursor, Uri contactUri) {
659            final String directoryParameter =
660                    contactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
661            final long directoryId = directoryParameter == null
662                    ? Directory.DEFAULT
663                    : Long.parseLong(directoryParameter);
664            final long contactId = cursor.getLong(ContactQuery.CONTACT_ID);
665            final String lookupKey = cursor.getString(ContactQuery.LOOKUP_KEY);
666            final long nameRawContactId = cursor.getLong(ContactQuery.NAME_RAW_CONTACT_ID);
667            final int displayNameSource = cursor.getInt(ContactQuery.DISPLAY_NAME_SOURCE);
668            final String displayName = cursor.getString(ContactQuery.DISPLAY_NAME);
669            final String phoneticName = cursor.getString(ContactQuery.PHONETIC_NAME);
670            final long photoId = cursor.getLong(ContactQuery.PHOTO_ID);
671            final String photoUri = cursor.getString(ContactQuery.PHOTO_URI);
672            final boolean starred = cursor.getInt(ContactQuery.STARRED) != 0;
673            final Integer presence = cursor.isNull(ContactQuery.CONTACT_PRESENCE)
674                    ? null
675                    : cursor.getInt(ContactQuery.CONTACT_PRESENCE);
676            final String status = cursor.getString(ContactQuery.CONTACT_STATUS);
677            final Long statusTimestamp = cursor.isNull(ContactQuery.CONTACT_STATUS_TIMESTAMP)
678                    ? null
679                    : cursor.getLong(ContactQuery.CONTACT_STATUS_TIMESTAMP);
680            final Integer statusLabel = cursor.isNull(ContactQuery.CONTACT_STATUS_LABEL)
681                    ? null
682                    : cursor.getInt(ContactQuery.CONTACT_STATUS_LABEL);
683            final String statusResPackage = cursor.getString(
684                    ContactQuery.CONTACT_STATUS_RES_PACKAGE);
685
686            Uri lookupUri;
687            if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) {
688                lookupUri = ContentUris.withAppendedId(
689                    Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), contactId);
690            } else {
691                lookupUri = contactUri;
692            }
693
694            return new Result(contactUri, lookupUri, directoryId, lookupKey, contactId,
695                    nameRawContactId, displayNameSource, photoId, photoUri, displayName,
696                    phoneticName, starred, presence, status, statusTimestamp, statusLabel,
697                    statusResPackage);
698        }
699
700        /**
701         * Extracts RawContact level columns from the cursor.
702         */
703        private ContentValues loadRawContact(Cursor cursor) {
704            ContentValues cv = new ContentValues();
705
706            cv.put(RawContacts._ID, cursor.getLong(ContactQuery.RAW_CONTACT_ID));
707
708            cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_NAME);
709            cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE);
710            cursorColumnToContentValues(cursor, cv, ContactQuery.DIRTY);
711            cursorColumnToContentValues(cursor, cv, ContactQuery.VERSION);
712            cursorColumnToContentValues(cursor, cv, ContactQuery.SOURCE_ID);
713            cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC1);
714            cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC2);
715            cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC3);
716            cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC4);
717            cursorColumnToContentValues(cursor, cv, ContactQuery.DELETED);
718            cursorColumnToContentValues(cursor, cv, ContactQuery.CONTACT_ID);
719            cursorColumnToContentValues(cursor, cv, ContactQuery.STARRED);
720            cursorColumnToContentValues(cursor, cv, ContactQuery.IS_RESTRICTED);
721            cursorColumnToContentValues(cursor, cv, ContactQuery.NAME_VERIFIED);
722
723            return cv;
724        }
725
726        /**
727         * Extracts Data level columns from the cursor.
728         */
729        private ContentValues loadData(Cursor cursor) {
730            ContentValues cv = new ContentValues();
731
732            cv.put(Data._ID, cursor.getLong(ContactQuery.DATA_ID));
733
734            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA1);
735            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA2);
736            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA3);
737            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA4);
738            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA5);
739            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA6);
740            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA7);
741            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA8);
742            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA9);
743            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA10);
744            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA11);
745            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA12);
746            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA13);
747            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA14);
748            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA15);
749            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC1);
750            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC2);
751            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC3);
752            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC4);
753            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_VERSION);
754            cursorColumnToContentValues(cursor, cv, ContactQuery.IS_PRIMARY);
755            cursorColumnToContentValues(cursor, cv, ContactQuery.IS_SUPERPRIMARY);
756            cursorColumnToContentValues(cursor, cv, ContactQuery.MIMETYPE);
757            cursorColumnToContentValues(cursor, cv, ContactQuery.RES_PACKAGE);
758            cursorColumnToContentValues(cursor, cv, ContactQuery.GROUP_SOURCE_ID);
759            cursorColumnToContentValues(cursor, cv, ContactQuery.CHAT_CAPABILITY);
760
761            return cv;
762        }
763
764        private void cursorColumnToContentValues(
765                Cursor cursor, ContentValues values, int index) {
766            switch (cursor.getType(index)) {
767                case Cursor.FIELD_TYPE_NULL:
768                    // don't put anything in the content values
769                    break;
770                case Cursor.FIELD_TYPE_INTEGER:
771                    values.put(ContactQuery.COLUMNS[index], cursor.getLong(index));
772                    break;
773                case Cursor.FIELD_TYPE_STRING:
774                    values.put(ContactQuery.COLUMNS[index], cursor.getString(index));
775                    break;
776                case Cursor.FIELD_TYPE_BLOB:
777                    values.put(ContactQuery.COLUMNS[index], cursor.getBlob(index));
778                    break;
779                default:
780                    throw new IllegalStateException("Invalid or unhandled data type");
781            }
782        }
783
784        private void loadDirectoryMetaData(Result result) {
785            long directoryId = result.getDirectoryId();
786
787            Cursor cursor = getContext().getContentResolver().query(
788                    ContentUris.withAppendedId(Directory.CONTENT_URI, directoryId),
789                    DirectoryQuery.COLUMNS, null, null, null);
790            if (cursor == null) {
791                return;
792            }
793            try {
794                if (cursor.moveToFirst()) {
795                    final String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME);
796                    final String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME);
797                    final int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID);
798                    final String accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
799                    final String accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
800                    final int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT);
801                    String directoryType = null;
802                    if (!TextUtils.isEmpty(packageName)) {
803                        PackageManager pm = getContext().getPackageManager();
804                        try {
805                            Resources resources = pm.getResourcesForApplication(packageName);
806                            directoryType = resources.getString(typeResourceId);
807                        } catch (NameNotFoundException e) {
808                            Log.w(TAG, "Contact directory resource not found: "
809                                    + packageName + "." + typeResourceId);
810                        }
811                    }
812
813                    result.setDirectoryMetaData(
814                            displayName, directoryType, accountType, accountName, exportSupport);
815                }
816            } finally {
817                cursor.close();
818            }
819        }
820
821        /**
822         * Loads groups meta-data for all groups associated with all constituent raw contacts'
823         * accounts.
824         */
825        private void loadGroupMetaData(Result result) {
826            StringBuilder selection = new StringBuilder();
827            ArrayList<String> selectionArgs = new ArrayList<String>();
828            for (Entity entity : result.mEntities) {
829                ContentValues values = entity.getEntityValues();
830                String accountName = values.getAsString(RawContacts.ACCOUNT_NAME);
831                String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
832                if (accountName != null && accountType != null) {
833                    if (selection.length() != 0) {
834                        selection.append(" OR ");
835                    }
836                    selection.append(
837                            "(" + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?)");
838                    selectionArgs.add(accountName);
839                    selectionArgs.add(accountType);
840                }
841            }
842            Cursor cursor = getContext().getContentResolver().query(Groups.CONTENT_URI,
843                    GroupQuery.COLUMNS, selection.toString(), selectionArgs.toArray(new String[0]),
844                    null);
845            try {
846                while (cursor.moveToNext()) {
847                    final String accountName = cursor.getString(GroupQuery.ACCOUNT_NAME);
848                    final String accountType = cursor.getString(GroupQuery.ACCOUNT_TYPE);
849                    final long groupId = cursor.getLong(GroupQuery.ID);
850                    final String title = cursor.getString(GroupQuery.TITLE);
851                    final boolean defaultGroup = cursor.isNull(GroupQuery.AUTO_ADD)
852                            ? false
853                            : cursor.getInt(GroupQuery.AUTO_ADD) != 0;
854                    final boolean favorites = cursor.isNull(GroupQuery.FAVORITES)
855                            ? false
856                            : cursor.getInt(GroupQuery.FAVORITES) != 0;
857
858                    result.addGroupMetaData(new GroupMetaData(
859                            accountName, accountType, groupId, title, defaultGroup, favorites));
860                }
861            } finally {
862                cursor.close();
863            }
864        }
865
866        @Override
867        protected void onPostExecute(Result result) {
868            unregisterObserver();
869
870            // The creator isn't interested in any further updates
871            if (mDestroyed || result == null) {
872                return;
873            }
874
875            mContact = result;
876
877            if (result != Result.ERROR && result != Result.NOT_FOUND) {
878                mLookupUri = result.getLookupUri();
879
880                if (!result.isDirectoryEntry()) {
881                    Log.i(TAG, "Registering content observer for " + mLookupUri);
882                    if (mObserver == null) {
883                        mObserver = new ForceLoadContentObserver();
884                    }
885                    getContext().getContentResolver().registerContentObserver(
886                            mLookupUri, true, mObserver);
887                }
888
889                if (mContact.getPhotoBinaryData() == null && mContact.getPhotoUri() != null) {
890                    mContact.setLoadingPhoto(true);
891                    new AsyncPhotoLoader().execute(mContact.getPhotoUri());
892                }
893            }
894
895            deliverResult(mContact);
896        }
897    }
898
899    private class AsyncPhotoLoader extends AsyncTask<String, Void, byte[]> {
900
901        private static final int BUFFER_SIZE = 1024*16;
902
903        @Override
904        protected byte[] doInBackground(String... params) {
905            Uri uri = Uri.parse(params[0]);
906            byte[] data = null;
907            try {
908                InputStream is = getContext().getContentResolver().openInputStream(uri);
909                if (is != null) {
910                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
911                    try {
912                        byte[] mBuffer = new byte[BUFFER_SIZE];
913
914                        int size;
915                        while ((size = is.read(mBuffer)) != -1) {
916                            baos.write(mBuffer, 0, size);
917                        }
918                        data = baos.toByteArray();
919                    } finally {
920                        is.close();
921                    }
922                } else {
923                    Log.v(TAG, "Cannot load photo " + uri);
924                }
925            } catch (IOException e) {
926                Log.e(TAG, "Cannot load photo " + uri, e);
927            }
928
929            return data;
930        }
931
932        @Override
933        protected void onPostExecute(byte[] data) {
934            if (mContact != null) {
935                mContact.setPhotoBinaryData(data);
936                mContact.setLoadingPhoto(false);
937                deliverResult(mContact);
938            }
939        }
940    }
941
942    private void unregisterObserver() {
943        if (mObserver != null) {
944            getContext().getContentResolver().unregisterContentObserver(mObserver);
945            mObserver = null;
946        }
947    }
948
949    public ContactLoader(Context context, Uri lookupUri) {
950        this(context, lookupUri, false);
951    }
952
953    public ContactLoader(Context context, Uri lookupUri, boolean loadGroupMetaData) {
954        super(context);
955        mLookupUri = lookupUri;
956        mLoadGroupMetaData = loadGroupMetaData;
957    }
958
959    public Uri getLookupUri() {
960        return mLookupUri;
961    }
962
963    @Override
964    protected void onStartLoading() {
965        if (mContact != null) {
966            deliverResult(mContact);
967        }
968
969        if (takeContentChanged() || mContact == null) {
970            forceLoad();
971        }
972    }
973
974    @Override
975    protected void onForceLoad() {
976        final LoadContactTask task = new LoadContactTask();
977        task.execute((Void[])null);
978    }
979
980    @Override
981    protected void onReset() {
982        unregisterObserver();
983        mContact = null;
984        mDestroyed = true;
985    }
986}
987