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