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