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