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