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.text.TextUtils;
39import android.util.Log;
40
41import com.android.contacts.GroupMetaData;
42import com.android.contacts.common.GeoUtil;
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.UriUtils;
48import com.android.contacts.model.dataitem.DataItem;
49import com.android.contacts.model.dataitem.PhoneDataItem;
50import com.android.contacts.model.dataitem.PhotoDataItem;
51import com.android.contacts.util.ContactLoaderUtils;
52import com.android.contacts.util.DataStatus;
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        try {
794            while (cursor.moveToNext()) {
795                final String accountName = cursor.getString(GroupQuery.ACCOUNT_NAME);
796                final String accountType = cursor.getString(GroupQuery.ACCOUNT_TYPE);
797                final String dataSet = cursor.getString(GroupQuery.DATA_SET);
798                final long groupId = cursor.getLong(GroupQuery.ID);
799                final String title = cursor.getString(GroupQuery.TITLE);
800                final boolean defaultGroup = cursor.isNull(GroupQuery.AUTO_ADD)
801                        ? false
802                        : cursor.getInt(GroupQuery.AUTO_ADD) != 0;
803                final boolean favorites = cursor.isNull(GroupQuery.FAVORITES)
804                        ? false
805                        : cursor.getInt(GroupQuery.FAVORITES) != 0;
806
807                groupListBuilder.add(new GroupMetaData(
808                        accountName, accountType, dataSet, groupId, title, defaultGroup,
809                        favorites));
810            }
811        } finally {
812            cursor.close();
813        }
814        result.setGroupMetaData(groupListBuilder.build());
815    }
816
817    /**
818     * Iterates over all data items that represent phone numbers are tries to calculate a formatted
819     * number. This function can safely be called several times as no unformatted data is
820     * overwritten
821     */
822    private void computeFormattedPhoneNumbers(Contact contactData) {
823        final String countryIso = GeoUtil.getCurrentCountryIso(getContext());
824        final ImmutableList<RawContact> rawContacts = contactData.getRawContacts();
825        final int rawContactCount = rawContacts.size();
826        for (int rawContactIndex = 0; rawContactIndex < rawContactCount; rawContactIndex++) {
827            final RawContact rawContact = rawContacts.get(rawContactIndex);
828            final List<DataItem> dataItems = rawContact.getDataItems();
829            final int dataCount = dataItems.size();
830            for (int dataIndex = 0; dataIndex < dataCount; dataIndex++) {
831                final DataItem dataItem = dataItems.get(dataIndex);
832                if (dataItem instanceof PhoneDataItem) {
833                    final PhoneDataItem phoneDataItem = (PhoneDataItem) dataItem;
834                    phoneDataItem.computeFormattedPhoneNumber(countryIso);
835                }
836            }
837        }
838    }
839
840    @Override
841    public void deliverResult(Contact result) {
842        unregisterObserver();
843
844        // The creator isn't interested in any further updates
845        if (isReset() || result == null) {
846            return;
847        }
848
849        mContact = result;
850
851        if (result.isLoaded()) {
852            mLookupUri = result.getLookupUri();
853
854            if (!result.isDirectoryEntry()) {
855                Log.i(TAG, "Registering content observer for " + mLookupUri);
856                if (mObserver == null) {
857                    mObserver = new ForceLoadContentObserver();
858                }
859                getContext().getContentResolver().registerContentObserver(
860                        mLookupUri, true, mObserver);
861            }
862
863            if (mPostViewNotification) {
864                // inform the source of the data that this contact is being looked at
865                postViewNotificationToSyncAdapter();
866            }
867        }
868
869        super.deliverResult(mContact);
870    }
871
872    /**
873     * Posts a message to the contributing sync adapters that have opted-in, notifying them
874     * that the contact has just been loaded
875     */
876    private void postViewNotificationToSyncAdapter() {
877        Context context = getContext();
878        for (RawContact rawContact : mContact.getRawContacts()) {
879            final long rawContactId = rawContact.getId();
880            if (mNotifiedRawContactIds.contains(rawContactId)) {
881                continue; // Already notified for this raw contact.
882            }
883            mNotifiedRawContactIds.add(rawContactId);
884            final AccountType accountType = rawContact.getAccountType(context);
885            final String serviceName = accountType.getViewContactNotifyServiceClassName();
886            final String servicePackageName = accountType.getViewContactNotifyServicePackageName();
887            if (!TextUtils.isEmpty(serviceName) && !TextUtils.isEmpty(servicePackageName)) {
888                final Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
889                final Intent intent = new Intent();
890                intent.setClassName(servicePackageName, serviceName);
891                intent.setAction(Intent.ACTION_VIEW);
892                intent.setDataAndType(uri, RawContacts.CONTENT_ITEM_TYPE);
893                try {
894                    context.startService(intent);
895                } catch (Exception e) {
896                    Log.e(TAG, "Error sending message to source-app", e);
897                }
898            }
899        }
900    }
901
902    private void unregisterObserver() {
903        if (mObserver != null) {
904            getContext().getContentResolver().unregisterContentObserver(mObserver);
905            mObserver = null;
906        }
907    }
908
909    /**
910     * Fully upgrades this ContactLoader to one with all lists fully loaded. When done, the
911     * new result will be delivered
912     */
913    public void upgradeToFullContact() {
914        // Everything requested already? Nothing to do, so let's bail out
915        if (mLoadGroupMetaData && mLoadInvitableAccountTypes
916                && mPostViewNotification && mComputeFormattedPhoneNumber) return;
917
918        mLoadGroupMetaData = true;
919        mLoadInvitableAccountTypes = true;
920        mPostViewNotification = true;
921        mComputeFormattedPhoneNumber = true;
922
923        // Cache the current result, so that we only load the "missing" parts of the contact.
924        cacheResult();
925
926        // Our load parameters have changed, so let's pretend the data has changed. Its the same
927        // thing, essentially.
928        onContentChanged();
929    }
930
931    public Uri getLookupUri() {
932        return mLookupUri;
933    }
934
935    @Override
936    protected void onStartLoading() {
937        if (mContact != null) {
938            deliverResult(mContact);
939        }
940
941        if (takeContentChanged() || mContact == null) {
942            forceLoad();
943        }
944    }
945
946    @Override
947    protected void onStopLoading() {
948        cancelLoad();
949    }
950
951    @Override
952    protected void onReset() {
953        super.onReset();
954        cancelLoad();
955        unregisterObserver();
956        mContact = null;
957    }
958
959    /**
960     * Caches the result, which is useful when we switch from activity to activity, using the same
961     * contact. If the next load is for a different contact, the cached result will be dropped
962     */
963    public void cacheResult() {
964        if (mContact == null || !mContact.isLoaded()) {
965            sCachedResult = null;
966        } else {
967            sCachedResult = mContact;
968        }
969    }
970}
971