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