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