Home | History | Annotate | Download | only in model
      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 
     17 package com.android.contacts.common.model;
     18 
     19 import android.content.AsyncTaskLoader;
     20 import android.content.ContentResolver;
     21 import android.content.ContentUris;
     22 import android.content.ContentValues;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.content.pm.PackageManager;
     26 import android.content.pm.PackageManager.NameNotFoundException;
     27 import android.content.res.AssetFileDescriptor;
     28 import android.content.res.Resources;
     29 import android.database.Cursor;
     30 import android.net.Uri;
     31 import android.provider.ContactsContract;
     32 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
     33 import android.provider.ContactsContract.Contacts;
     34 import android.provider.ContactsContract.Data;
     35 import android.provider.ContactsContract.Directory;
     36 import android.provider.ContactsContract.Groups;
     37 import android.provider.ContactsContract.RawContacts;
     38 import android.text.TextUtils;
     39 import android.util.Log;
     40 
     41 import com.android.contacts.common.GeoUtil;
     42 import com.android.contacts.common.GroupMetaData;
     43 import com.android.contacts.common.compat.CompatUtils;
     44 import com.android.contacts.common.model.account.AccountType;
     45 import com.android.contacts.common.model.account.AccountTypeWithDataSet;
     46 import com.android.contacts.common.util.Constants;
     47 import com.android.contacts.common.util.ContactLoaderUtils;
     48 import com.android.contacts.common.util.DataStatus;
     49 import com.android.contacts.common.util.UriUtils;
     50 import com.android.contacts.common.model.dataitem.DataItem;
     51 import com.android.contacts.common.model.dataitem.PhoneDataItem;
     52 import com.android.contacts.common.model.dataitem.PhotoDataItem;
     53 import com.google.common.collect.ImmutableList;
     54 import com.google.common.collect.ImmutableMap;
     55 import com.google.common.collect.Lists;
     56 import com.google.common.collect.Maps;
     57 import com.google.common.collect.Sets;
     58 
     59 import org.json.JSONArray;
     60 import org.json.JSONException;
     61 import org.json.JSONObject;
     62 
     63 import java.io.ByteArrayOutputStream;
     64 import java.io.IOException;
     65 import java.io.InputStream;
     66 import java.net.URL;
     67 import java.util.ArrayList;
     68 import java.util.HashSet;
     69 import java.util.Iterator;
     70 import java.util.List;
     71 import java.util.Map;
     72 import java.util.Objects;
     73 import java.util.Set;
     74 
     75 /**
     76  * Loads a single Contact and all it constituent RawContacts.
     77  */
     78 public 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