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