Home | History | Annotate | Download | only in contacts
      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;
     18 
     19 import com.android.contacts.model.AccountType;
     20 import com.android.contacts.model.AccountTypeManager;
     21 import com.android.contacts.model.AccountTypeWithDataSet;
     22 import com.android.contacts.model.EntityDeltaList;
     23 import com.android.contacts.util.ContactLoaderUtils;
     24 import com.android.contacts.util.DataStatus;
     25 import com.android.contacts.util.StreamItemEntry;
     26 import com.android.contacts.util.StreamItemPhotoEntry;
     27 import com.android.contacts.util.UriUtils;
     28 import com.google.common.annotations.VisibleForTesting;
     29 import com.google.common.collect.Lists;
     30 import com.google.common.collect.Maps;
     31 import com.google.common.collect.Sets;
     32 
     33 import android.content.AsyncTaskLoader;
     34 import android.content.ContentResolver;
     35 import android.content.ContentUris;
     36 import android.content.ContentValues;
     37 import android.content.Context;
     38 import android.content.Entity;
     39 import android.content.Entity.NamedContentValues;
     40 import android.content.Intent;
     41 import android.content.pm.PackageManager;
     42 import android.content.pm.PackageManager.NameNotFoundException;
     43 import android.content.res.AssetFileDescriptor;
     44 import android.content.res.Resources;
     45 import android.database.Cursor;
     46 import android.net.Uri;
     47 import android.provider.ContactsContract;
     48 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
     49 import android.provider.ContactsContract.CommonDataKinds.Photo;
     50 import android.provider.ContactsContract.Contacts;
     51 import android.provider.ContactsContract.Data;
     52 import android.provider.ContactsContract.Directory;
     53 import android.provider.ContactsContract.DisplayNameSources;
     54 import android.provider.ContactsContract.Groups;
     55 import android.provider.ContactsContract.RawContacts;
     56 import android.provider.ContactsContract.StreamItemPhotos;
     57 import android.provider.ContactsContract.StreamItems;
     58 import android.text.TextUtils;
     59 import android.util.Log;
     60 import android.util.LongSparseArray;
     61 
     62 import java.io.ByteArrayOutputStream;
     63 import java.io.FileInputStream;
     64 import java.io.IOException;
     65 import java.util.ArrayList;
     66 import java.util.Collections;
     67 import java.util.List;
     68 import java.util.Map;
     69 import java.util.Set;
     70 
     71 /**
     72  * Loads a single Contact and all it constituent RawContacts.
     73  */
     74 public class ContactLoader extends AsyncTaskLoader<ContactLoader.Result> {
     75     private static final String TAG = ContactLoader.class.getSimpleName();
     76 
     77     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     78 
     79     /** A short-lived cache that can be set by {@link #cacheResult()} */
     80     private static Result sCachedResult = null;
     81 
     82     private final Uri mRequestedUri;
     83     private Uri mLookupUri;
     84     private boolean mLoadGroupMetaData;
     85     private boolean mLoadStreamItems;
     86     private boolean mLoadInvitableAccountTypes;
     87     private boolean mPostViewNotification;
     88     private Result 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);
     94     }
     95 
     96     public ContactLoader(Context context, Uri lookupUri, boolean loadGroupMetaData,
     97             boolean loadStreamItems, boolean loadInvitableAccountTypes,
     98             boolean postViewNotification) {
     99         super(context);
    100         mLookupUri = lookupUri;
    101         mRequestedUri = lookupUri;
    102         mLoadGroupMetaData = loadGroupMetaData;
    103         mLoadStreamItems = loadStreamItems;
    104         mLoadInvitableAccountTypes = loadInvitableAccountTypes;
    105         mPostViewNotification = postViewNotification;
    106     }
    107 
    108     /**
    109      * The result of a load operation. Contains all data necessary to display the contact.
    110      */
    111     public static final class Result {
    112         private enum Status {
    113             /** Contact is successfully loaded */
    114             LOADED,
    115             /** There was an error loading the contact */
    116             ERROR,
    117             /** Contact is not found */
    118             NOT_FOUND,
    119         }
    120 
    121         private final Uri mRequestedUri;
    122         private final Uri mLookupUri;
    123         private final Uri mUri;
    124         private final long mDirectoryId;
    125         private final String mLookupKey;
    126         private final long mId;
    127         private final long mNameRawContactId;
    128         private final int mDisplayNameSource;
    129         private final long mPhotoId;
    130         private final String mPhotoUri;
    131         private final String mDisplayName;
    132         private final String mAltDisplayName;
    133         private final String mPhoneticName;
    134         private final boolean mStarred;
    135         private final Integer mPresence;
    136         private final ArrayList<Entity> mEntities;
    137         private ArrayList<StreamItemEntry> mStreamItems;
    138         private final LongSparseArray<DataStatus> mStatuses;
    139         private ArrayList<AccountType> mInvitableAccountTypes;
    140 
    141         private String mDirectoryDisplayName;
    142         private String mDirectoryType;
    143         private String mDirectoryAccountType;
    144         private String mDirectoryAccountName;
    145         private int mDirectoryExportSupport;
    146 
    147         private ArrayList<GroupMetaData> mGroups;
    148 
    149         private byte[] mPhotoBinaryData;
    150         private final boolean mSendToVoicemail;
    151         private final String mCustomRingtone;
    152         private final boolean mIsUserProfile;
    153 
    154         private final Status mStatus;
    155         private final Exception mException;
    156 
    157         /**
    158          * Constructor for special results, namely "no contact found" and "error".
    159          */
    160         private Result(Uri requestedUri, Status status, Exception exception) {
    161             if (status == Status.ERROR && exception == null) {
    162                 throw new IllegalArgumentException("ERROR result must have exception");
    163             }
    164             mStatus = status;
    165             mException = exception;
    166             mRequestedUri = requestedUri;
    167             mLookupUri = null;
    168             mUri = null;
    169             mDirectoryId = -1;
    170             mLookupKey = null;
    171             mId = -1;
    172             mEntities = null;
    173             mStreamItems = null;
    174             mStatuses = null;
    175             mNameRawContactId = -1;
    176             mDisplayNameSource = DisplayNameSources.UNDEFINED;
    177             mPhotoId = -1;
    178             mPhotoUri = null;
    179             mDisplayName = null;
    180             mAltDisplayName = null;
    181             mPhoneticName = null;
    182             mStarred = false;
    183             mPresence = null;
    184             mInvitableAccountTypes = null;
    185             mSendToVoicemail = false;
    186             mCustomRingtone = null;
    187             mIsUserProfile = false;
    188         }
    189 
    190         private static Result forError(Uri requestedUri, Exception exception) {
    191             return new Result(requestedUri, Status.ERROR, exception);
    192         }
    193 
    194         private static Result forNotFound(Uri requestedUri) {
    195             return new Result(requestedUri, Status.NOT_FOUND, null);
    196         }
    197 
    198         /**
    199          * Constructor to call when contact was found
    200          */
    201         private Result(Uri requestedUri, Uri uri, Uri lookupUri, long directoryId, String lookupKey,
    202                 long id, long nameRawContactId, int displayNameSource, long photoId,
    203                 String photoUri, String displayName, String altDisplayName, String phoneticName,
    204                 boolean starred, Integer presence, boolean sendToVoicemail, String customRingtone,
    205                 boolean isUserProfile) {
    206             mStatus = Status.LOADED;
    207             mException = null;
    208             mRequestedUri = requestedUri;
    209             mLookupUri = lookupUri;
    210             mUri = uri;
    211             mDirectoryId = directoryId;
    212             mLookupKey = lookupKey;
    213             mId = id;
    214             mEntities = new ArrayList<Entity>();
    215             mStreamItems = null;
    216             mStatuses = new LongSparseArray<DataStatus>();
    217             mNameRawContactId = nameRawContactId;
    218             mDisplayNameSource = displayNameSource;
    219             mPhotoId = photoId;
    220             mPhotoUri = photoUri;
    221             mDisplayName = displayName;
    222             mAltDisplayName = altDisplayName;
    223             mPhoneticName = phoneticName;
    224             mStarred = starred;
    225             mPresence = presence;
    226             mInvitableAccountTypes = null;
    227             mSendToVoicemail = sendToVoicemail;
    228             mCustomRingtone = customRingtone;
    229             mIsUserProfile = isUserProfile;
    230         }
    231 
    232         private Result(Uri requestedUri, Result from) {
    233             mRequestedUri = requestedUri;
    234 
    235             mStatus = from.mStatus;
    236             mException = from.mException;
    237             mLookupUri = from.mLookupUri;
    238             mUri = from.mUri;
    239             mDirectoryId = from.mDirectoryId;
    240             mLookupKey = from.mLookupKey;
    241             mId = from.mId;
    242             mNameRawContactId = from.mNameRawContactId;
    243             mDisplayNameSource = from.mDisplayNameSource;
    244             mPhotoId = from.mPhotoId;
    245             mPhotoUri = from.mPhotoUri;
    246             mDisplayName = from.mDisplayName;
    247             mAltDisplayName = from.mAltDisplayName;
    248             mPhoneticName = from.mPhoneticName;
    249             mStarred = from.mStarred;
    250             mPresence = from.mPresence;
    251             mEntities = from.mEntities;
    252             mStreamItems = from.mStreamItems;
    253             mStatuses = from.mStatuses;
    254             mInvitableAccountTypes = from.mInvitableAccountTypes;
    255 
    256             mDirectoryDisplayName = from.mDirectoryDisplayName;
    257             mDirectoryType = from.mDirectoryType;
    258             mDirectoryAccountType = from.mDirectoryAccountType;
    259             mDirectoryAccountName = from.mDirectoryAccountName;
    260             mDirectoryExportSupport = from.mDirectoryExportSupport;
    261 
    262             mGroups = from.mGroups;
    263 
    264             mPhotoBinaryData = from.mPhotoBinaryData;
    265             mSendToVoicemail = from.mSendToVoicemail;
    266             mCustomRingtone = from.mCustomRingtone;
    267             mIsUserProfile = from.mIsUserProfile;
    268         }
    269 
    270         /**
    271          * @param exportSupport See {@link Directory#EXPORT_SUPPORT}.
    272          */
    273         private void setDirectoryMetaData(String displayName, String directoryType,
    274                 String accountType, String accountName, int exportSupport) {
    275             mDirectoryDisplayName = displayName;
    276             mDirectoryType = directoryType;
    277             mDirectoryAccountType = accountType;
    278             mDirectoryAccountName = accountName;
    279             mDirectoryExportSupport = exportSupport;
    280         }
    281 
    282         private void setPhotoBinaryData(byte[] photoBinaryData) {
    283             mPhotoBinaryData = photoBinaryData;
    284         }
    285 
    286         /**
    287          * Returns the URI for the contact that contains both the lookup key and the ID. This is
    288          * the best URI to reference a contact.
    289          * For directory contacts, this is the same a the URI as returned by {@link #getUri()}
    290          */
    291         public Uri getLookupUri() {
    292             return mLookupUri;
    293         }
    294 
    295         public String getLookupKey() {
    296             return mLookupKey;
    297         }
    298 
    299         /**
    300          * Returns the contact Uri that was passed to the provider to make the query. This is
    301          * the same as the requested Uri, unless the requested Uri doesn't specify a Contact:
    302          * If it either references a Raw-Contact or a Person (a pre-Eclair style Uri), this Uri will
    303          * always reference the full aggregate contact.
    304          */
    305         public Uri getUri() {
    306             return mUri;
    307         }
    308 
    309         /**
    310          * Returns the URI for which this {@link ContactLoader) was initially requested.
    311          */
    312         public Uri getRequestedUri() {
    313             return mRequestedUri;
    314         }
    315 
    316         /**
    317          * Instantiate a new EntityDeltaList for this contact.
    318          */
    319         public EntityDeltaList createEntityDeltaList() {
    320             return EntityDeltaList.fromIterator(getEntities().iterator());
    321         }
    322 
    323         /**
    324          * Returns the contact ID.
    325          */
    326         @VisibleForTesting
    327         /* package */ long getId() {
    328             return mId;
    329         }
    330 
    331         /**
    332          * @return true when an exception happened during loading, in which case
    333          *     {@link #getException} returns the actual exception object.
    334          *     Note {@link #isNotFound()} and {@link #isError()} are mutually exclusive; If
    335          *     {@link #isError()} is {@code true}, {@link #isNotFound()} is always {@code false},
    336          *     and vice versa.
    337          */
    338         public boolean isError() {
    339             return mStatus == Status.ERROR;
    340         }
    341 
    342         public Exception getException() {
    343             return mException;
    344         }
    345 
    346         /**
    347          * @return true when the specified contact is not found.
    348          *     Note {@link #isNotFound()} and {@link #isError()} are mutually exclusive; If
    349          *     {@link #isError()} is {@code true}, {@link #isNotFound()} is always {@code false},
    350          *     and vice versa.
    351          */
    352         public boolean isNotFound() {
    353             return mStatus == Status.NOT_FOUND;
    354         }
    355 
    356         /**
    357          * @return true if the specified contact is successfully loaded.
    358          *     i.e. neither {@link #isError()} nor {@link #isNotFound()}.
    359          */
    360         public boolean isLoaded() {
    361             return mStatus == Status.LOADED;
    362         }
    363 
    364         public long getNameRawContactId() {
    365             return mNameRawContactId;
    366         }
    367 
    368         public int getDisplayNameSource() {
    369             return mDisplayNameSource;
    370         }
    371 
    372         public long getPhotoId() {
    373             return mPhotoId;
    374         }
    375 
    376         public String getPhotoUri() {
    377             return mPhotoUri;
    378         }
    379 
    380         public String getDisplayName() {
    381             return mDisplayName;
    382         }
    383 
    384         public String getAltDisplayName() {
    385             return mAltDisplayName;
    386         }
    387 
    388         public String getPhoneticName() {
    389             return mPhoneticName;
    390         }
    391 
    392         public boolean getStarred() {
    393             return mStarred;
    394         }
    395 
    396         public Integer getPresence() {
    397             return mPresence;
    398         }
    399 
    400         public ArrayList<AccountType> getInvitableAccountTypes() {
    401             return mInvitableAccountTypes;
    402         }
    403 
    404         public ArrayList<Entity> getEntities() {
    405             return mEntities;
    406         }
    407 
    408         public ArrayList<StreamItemEntry> getStreamItems() {
    409             return mStreamItems;
    410         }
    411 
    412         public LongSparseArray<DataStatus> getStatuses() {
    413             return mStatuses;
    414         }
    415 
    416         public long getDirectoryId() {
    417             return mDirectoryId;
    418         }
    419 
    420         public boolean isDirectoryEntry() {
    421             return mDirectoryId != -1 && mDirectoryId != Directory.DEFAULT
    422                     && mDirectoryId != Directory.LOCAL_INVISIBLE;
    423         }
    424 
    425         /**
    426          * @return true if this is a contact (not group, etc.) with at least one
    427          *         writable raw-contact, and false otherwise.
    428          */
    429         public boolean isWritableContact(final Context context) {
    430             return getFirstWritableRawContactId(context) != -1;
    431         }
    432 
    433         /**
    434          * Return the ID of the first raw-contact in the contact data that belongs to a
    435          * contact-writable account, or -1 if no such entity exists.
    436          */
    437         public long getFirstWritableRawContactId(final Context context) {
    438             // Directory entries are non-writable
    439             if (isDirectoryEntry()) return -1;
    440 
    441             // Iterate through raw-contacts; if we find a writable on, return its ID.
    442             final AccountTypeManager accountTypes = AccountTypeManager.getInstance(context);
    443             for (Entity entity : getEntities()) {
    444                 ContentValues values = entity.getEntityValues();
    445                 String type = values.getAsString(RawContacts.ACCOUNT_TYPE);
    446                 String dataSet = values.getAsString(RawContacts.DATA_SET);
    447 
    448                 AccountType accountType = accountTypes.getAccountType(type, dataSet);
    449                 if (accountType != null && accountType.areContactsWritable()) {
    450                     return values.getAsLong(RawContacts._ID);
    451                 }
    452             }
    453             // No writable raw-contact was found.
    454             return -1;
    455         }
    456 
    457         public int getDirectoryExportSupport() {
    458             return mDirectoryExportSupport;
    459         }
    460 
    461         public String getDirectoryDisplayName() {
    462             return mDirectoryDisplayName;
    463         }
    464 
    465         public String getDirectoryType() {
    466             return mDirectoryType;
    467         }
    468 
    469         public String getDirectoryAccountType() {
    470             return mDirectoryAccountType;
    471         }
    472 
    473         public String getDirectoryAccountName() {
    474             return mDirectoryAccountName;
    475         }
    476 
    477         public byte[] getPhotoBinaryData() {
    478             return mPhotoBinaryData;
    479         }
    480 
    481         public ArrayList<ContentValues> getContentValues() {
    482             if (mEntities.size() != 1) {
    483                 throw new IllegalStateException(
    484                         "Cannot extract content values from an aggregated contact");
    485             }
    486 
    487             Entity entity = mEntities.get(0);
    488             ArrayList<ContentValues> result = new ArrayList<ContentValues>();
    489             ArrayList<NamedContentValues> subValues = entity.getSubValues();
    490             if (subValues != null) {
    491                 int size = subValues.size();
    492                 for (int i = 0; i < size; i++) {
    493                     NamedContentValues pair = subValues.get(i);
    494                     if (Data.CONTENT_URI.equals(pair.uri)) {
    495                         result.add(pair.values);
    496                     }
    497                 }
    498             }
    499 
    500             // If the photo was loaded using the URI, create an entry for the photo
    501             // binary data.
    502             if (mPhotoId == 0 && mPhotoBinaryData != null) {
    503                 ContentValues photo = new ContentValues();
    504                 photo.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
    505                 photo.put(Photo.PHOTO, mPhotoBinaryData);
    506                 result.add(photo);
    507             }
    508 
    509             return result;
    510         }
    511 
    512         public List<GroupMetaData> getGroupMetaData() {
    513             return mGroups;
    514         }
    515 
    516         public boolean isSendToVoicemail() {
    517             return mSendToVoicemail;
    518         }
    519 
    520         public String getCustomRingtone() {
    521             return mCustomRingtone;
    522         }
    523 
    524         public boolean isUserProfile() {
    525             return mIsUserProfile;
    526         }
    527 
    528         @Override
    529         public String toString() {
    530             return "{requested=" + mRequestedUri + ",lookupkey=" + mLookupKey +
    531                     ",uri=" + mUri + ",status=" + mStatus + "}";
    532         }
    533     }
    534 
    535     /**
    536      * Projection used for the query that loads all data for the entire contact (except for
    537      * social stream items).
    538      */
    539     private static class ContactQuery {
    540         final static String[] COLUMNS = new String[] {
    541                 Contacts.NAME_RAW_CONTACT_ID,
    542                 Contacts.DISPLAY_NAME_SOURCE,
    543                 Contacts.LOOKUP_KEY,
    544                 Contacts.DISPLAY_NAME,
    545                 Contacts.DISPLAY_NAME_ALTERNATIVE,
    546                 Contacts.PHONETIC_NAME,
    547                 Contacts.PHOTO_ID,
    548                 Contacts.STARRED,
    549                 Contacts.CONTACT_PRESENCE,
    550                 Contacts.CONTACT_STATUS,
    551                 Contacts.CONTACT_STATUS_TIMESTAMP,
    552                 Contacts.CONTACT_STATUS_RES_PACKAGE,
    553                 Contacts.CONTACT_STATUS_LABEL,
    554                 Contacts.Entity.CONTACT_ID,
    555                 Contacts.Entity.RAW_CONTACT_ID,
    556 
    557                 RawContacts.ACCOUNT_NAME,
    558                 RawContacts.ACCOUNT_TYPE,
    559                 RawContacts.DATA_SET,
    560                 RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
    561                 RawContacts.DIRTY,
    562                 RawContacts.VERSION,
    563                 RawContacts.SOURCE_ID,
    564                 RawContacts.SYNC1,
    565                 RawContacts.SYNC2,
    566                 RawContacts.SYNC3,
    567                 RawContacts.SYNC4,
    568                 RawContacts.DELETED,
    569                 RawContacts.NAME_VERIFIED,
    570 
    571                 Contacts.Entity.DATA_ID,
    572                 Data.DATA1,
    573                 Data.DATA2,
    574                 Data.DATA3,
    575                 Data.DATA4,
    576                 Data.DATA5,
    577                 Data.DATA6,
    578                 Data.DATA7,
    579                 Data.DATA8,
    580                 Data.DATA9,
    581                 Data.DATA10,
    582                 Data.DATA11,
    583                 Data.DATA12,
    584                 Data.DATA13,
    585                 Data.DATA14,
    586                 Data.DATA15,
    587                 Data.SYNC1,
    588                 Data.SYNC2,
    589                 Data.SYNC3,
    590                 Data.SYNC4,
    591                 Data.DATA_VERSION,
    592                 Data.IS_PRIMARY,
    593                 Data.IS_SUPER_PRIMARY,
    594                 Data.MIMETYPE,
    595                 Data.RES_PACKAGE,
    596 
    597                 GroupMembership.GROUP_SOURCE_ID,
    598 
    599                 Data.PRESENCE,
    600                 Data.CHAT_CAPABILITY,
    601                 Data.STATUS,
    602                 Data.STATUS_RES_PACKAGE,
    603                 Data.STATUS_ICON,
    604                 Data.STATUS_LABEL,
    605                 Data.STATUS_TIMESTAMP,
    606 
    607                 Contacts.PHOTO_URI,
    608                 Contacts.SEND_TO_VOICEMAIL,
    609                 Contacts.CUSTOM_RINGTONE,
    610                 Contacts.IS_USER_PROFILE,
    611         };
    612 
    613         public final static int NAME_RAW_CONTACT_ID = 0;
    614         public final static int DISPLAY_NAME_SOURCE = 1;
    615         public final static int LOOKUP_KEY = 2;
    616         public final static int DISPLAY_NAME = 3;
    617         public final static int ALT_DISPLAY_NAME = 4;
    618         public final static int PHONETIC_NAME = 5;
    619         public final static int PHOTO_ID = 6;
    620         public final static int STARRED = 7;
    621         public final static int CONTACT_PRESENCE = 8;
    622         public final static int CONTACT_STATUS = 9;
    623         public final static int CONTACT_STATUS_TIMESTAMP = 10;
    624         public final static int CONTACT_STATUS_RES_PACKAGE = 11;
    625         public final static int CONTACT_STATUS_LABEL = 12;
    626         public final static int CONTACT_ID = 13;
    627         public final static int RAW_CONTACT_ID = 14;
    628 
    629         public final static int ACCOUNT_NAME = 15;
    630         public final static int ACCOUNT_TYPE = 16;
    631         public final static int DATA_SET = 17;
    632         public final static int ACCOUNT_TYPE_AND_DATA_SET = 18;
    633         public final static int DIRTY = 19;
    634         public final static int VERSION = 20;
    635         public final static int SOURCE_ID = 21;
    636         public final static int SYNC1 = 22;
    637         public final static int SYNC2 = 23;
    638         public final static int SYNC3 = 24;
    639         public final static int SYNC4 = 25;
    640         public final static int DELETED = 26;
    641         public final static int NAME_VERIFIED = 27;
    642 
    643         public final static int DATA_ID = 28;
    644         public final static int DATA1 = 29;
    645         public final static int DATA2 = 30;
    646         public final static int DATA3 = 31;
    647         public final static int DATA4 = 32;
    648         public final static int DATA5 = 33;
    649         public final static int DATA6 = 34;
    650         public final static int DATA7 = 35;
    651         public final static int DATA8 = 36;
    652         public final static int DATA9 = 37;
    653         public final static int DATA10 = 38;
    654         public final static int DATA11 = 39;
    655         public final static int DATA12 = 40;
    656         public final static int DATA13 = 41;
    657         public final static int DATA14 = 42;
    658         public final static int DATA15 = 43;
    659         public final static int DATA_SYNC1 = 44;
    660         public final static int DATA_SYNC2 = 45;
    661         public final static int DATA_SYNC3 = 46;
    662         public final static int DATA_SYNC4 = 47;
    663         public final static int DATA_VERSION = 48;
    664         public final static int IS_PRIMARY = 49;
    665         public final static int IS_SUPERPRIMARY = 50;
    666         public final static int MIMETYPE = 51;
    667         public final static int RES_PACKAGE = 52;
    668 
    669         public final static int GROUP_SOURCE_ID = 53;
    670 
    671         public final static int PRESENCE = 54;
    672         public final static int CHAT_CAPABILITY = 55;
    673         public final static int STATUS = 56;
    674         public final static int STATUS_RES_PACKAGE = 57;
    675         public final static int STATUS_ICON = 58;
    676         public final static int STATUS_LABEL = 59;
    677         public final static int STATUS_TIMESTAMP = 60;
    678 
    679         public final static int PHOTO_URI = 61;
    680         public final static int SEND_TO_VOICEMAIL = 62;
    681         public final static int CUSTOM_RINGTONE = 63;
    682         public final static int IS_USER_PROFILE = 64;
    683     }
    684 
    685     /**
    686      * Projection used for the query that loads all data for the entire contact.
    687      */
    688     private static class DirectoryQuery {
    689         final static String[] COLUMNS = new String[] {
    690             Directory.DISPLAY_NAME,
    691             Directory.PACKAGE_NAME,
    692             Directory.TYPE_RESOURCE_ID,
    693             Directory.ACCOUNT_TYPE,
    694             Directory.ACCOUNT_NAME,
    695             Directory.EXPORT_SUPPORT,
    696         };
    697 
    698         public final static int DISPLAY_NAME = 0;
    699         public final static int PACKAGE_NAME = 1;
    700         public final static int TYPE_RESOURCE_ID = 2;
    701         public final static int ACCOUNT_TYPE = 3;
    702         public final static int ACCOUNT_NAME = 4;
    703         public final static int EXPORT_SUPPORT = 5;
    704     }
    705 
    706     private static class GroupQuery {
    707         final static String[] COLUMNS = new String[] {
    708             Groups.ACCOUNT_NAME,
    709             Groups.ACCOUNT_TYPE,
    710             Groups.DATA_SET,
    711             Groups.ACCOUNT_TYPE_AND_DATA_SET,
    712             Groups._ID,
    713             Groups.TITLE,
    714             Groups.AUTO_ADD,
    715             Groups.FAVORITES,
    716         };
    717 
    718         public final static int ACCOUNT_NAME = 0;
    719         public final static int ACCOUNT_TYPE = 1;
    720         public final static int DATA_SET = 2;
    721         public final static int ACCOUNT_TYPE_AND_DATA_SET = 3;
    722         public final static int ID = 4;
    723         public final static int TITLE = 5;
    724         public final static int AUTO_ADD = 6;
    725         public final static int FAVORITES = 7;
    726     }
    727 
    728     @Override
    729     public Result loadInBackground() {
    730         try {
    731             final ContentResolver resolver = getContext().getContentResolver();
    732             final Uri uriCurrentFormat = ContactLoaderUtils.ensureIsContactUri(
    733                     resolver, mLookupUri);
    734             final Result cachedResult = sCachedResult;
    735             sCachedResult = null;
    736             // Is this the same Uri as what we had before already? In that case, reuse that result
    737             final Result result;
    738             final boolean resultIsCached;
    739             if (cachedResult != null &&
    740                     UriUtils.areEqual(cachedResult.getLookupUri(), mLookupUri)) {
    741                 // We are using a cached result from earlier. Below, we should make sure
    742                 // we are not doing any more network or disc accesses
    743                 result = new Result(mRequestedUri, cachedResult);
    744                 resultIsCached = true;
    745             } else {
    746                 result = loadContactEntity(resolver, uriCurrentFormat);
    747                 resultIsCached = false;
    748             }
    749             if (result.isLoaded()) {
    750                 if (result.isDirectoryEntry()) {
    751                     if (!resultIsCached) {
    752                         loadDirectoryMetaData(result);
    753                     }
    754                 } else if (mLoadGroupMetaData) {
    755                     if (result.getGroupMetaData() == null) {
    756                         loadGroupMetaData(result);
    757                     }
    758                 }
    759                 if (mLoadStreamItems && result.getStreamItems() == null) {
    760                     loadStreamItems(result);
    761                 }
    762                 if (!resultIsCached) loadPhotoBinaryData(result);
    763 
    764                 // Note ME profile should never have "Add connection"
    765                 if (mLoadInvitableAccountTypes && result.getInvitableAccountTypes() == null) {
    766                     loadInvitableAccountTypes(result);
    767                 }
    768             }
    769             return result;
    770         } catch (Exception e) {
    771             Log.e(TAG, "Error loading the contact: " + mLookupUri, e);
    772             return Result.forError(mRequestedUri, e);
    773         }
    774     }
    775 
    776     private Result loadContactEntity(ContentResolver resolver, Uri contactUri) {
    777         Uri entityUri = Uri.withAppendedPath(contactUri, Contacts.Entity.CONTENT_DIRECTORY);
    778         Cursor cursor = resolver.query(entityUri, ContactQuery.COLUMNS, null, null,
    779                 Contacts.Entity.RAW_CONTACT_ID);
    780         if (cursor == null) {
    781             Log.e(TAG, "No cursor returned in loadContactEntity");
    782             return Result.forNotFound(mRequestedUri);
    783         }
    784 
    785         try {
    786             if (!cursor.moveToFirst()) {
    787                 cursor.close();
    788                 return Result.forNotFound(mRequestedUri);
    789             }
    790 
    791             // Create the loaded result starting with the Contact data.
    792             Result result = loadContactHeaderData(cursor, contactUri);
    793 
    794             // Fill in the raw contacts, which is wrapped in an Entity and any
    795             // status data.  Initially, result has empty entities and statuses.
    796             long currentRawContactId = -1;
    797             Entity entity = null;
    798             ArrayList<Entity> entities = result.getEntities();
    799             LongSparseArray<DataStatus> statuses = result.getStatuses();
    800             for (; !cursor.isAfterLast(); cursor.moveToNext()) {
    801                 long rawContactId = cursor.getLong(ContactQuery.RAW_CONTACT_ID);
    802                 if (rawContactId != currentRawContactId) {
    803                     // First time to see this raw contact id, so create a new entity, and
    804                     // add it to the result's entities.
    805                     currentRawContactId = rawContactId;
    806                     entity = new android.content.Entity(loadRawContact(cursor));
    807                     entities.add(entity);
    808                 }
    809                 if (!cursor.isNull(ContactQuery.DATA_ID)) {
    810                     ContentValues data = loadData(cursor);
    811                     entity.addSubValue(ContactsContract.Data.CONTENT_URI, data);
    812 
    813                     if (!cursor.isNull(ContactQuery.PRESENCE)
    814                             || !cursor.isNull(ContactQuery.STATUS)) {
    815                         final DataStatus status = new DataStatus(cursor);
    816                         final long dataId = cursor.getLong(ContactQuery.DATA_ID);
    817                         statuses.put(dataId, status);
    818                     }
    819                 }
    820             }
    821 
    822             return result;
    823         } finally {
    824             cursor.close();
    825         }
    826     }
    827 
    828     /**
    829      * Looks for the photo data item in entities. If found, creates a new Bitmap instance. If
    830      * not found, returns null
    831      */
    832     private void loadPhotoBinaryData(Result contactData) {
    833 
    834         // If we have a photo URI, try loading that first.
    835         String photoUri = contactData.getPhotoUri();
    836         if (photoUri != null) {
    837             try {
    838                 AssetFileDescriptor fd = getContext().getContentResolver()
    839                        .openAssetFileDescriptor(Uri.parse(photoUri), "r");
    840                 byte[] buffer = new byte[16 * 1024];
    841                 FileInputStream fis = fd.createInputStream();
    842                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
    843                 try {
    844                     int size;
    845                     while ((size = fis.read(buffer)) != -1) {
    846                         baos.write(buffer, 0, size);
    847                     }
    848                     contactData.setPhotoBinaryData(baos.toByteArray());
    849                 } finally {
    850                     fis.close();
    851                     fd.close();
    852                 }
    853                 return;
    854             } catch (IOException ioe) {
    855                 // Just fall back to the case below.
    856             }
    857         }
    858 
    859         // If we couldn't load from a file, fall back to the data blob.
    860         final long photoId = contactData.getPhotoId();
    861         if (photoId <= 0) {
    862             // No photo ID
    863             return;
    864         }
    865 
    866         for (Entity entity : contactData.getEntities()) {
    867             for (NamedContentValues subValue : entity.getSubValues()) {
    868                 final ContentValues entryValues = subValue.values;
    869                 final long dataId = entryValues.getAsLong(Data._ID);
    870                 if (dataId == photoId) {
    871                     final String mimeType = entryValues.getAsString(Data.MIMETYPE);
    872                     // Correct Data Id but incorrect MimeType? Don't load
    873                     if (!Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
    874                         return;
    875                     }
    876                     contactData.setPhotoBinaryData(entryValues.getAsByteArray(Photo.PHOTO));
    877                     break;
    878                 }
    879             }
    880         }
    881     }
    882 
    883     /**
    884      * Sets the "invitable" account types to {@link Result#mInvitableAccountTypes}.
    885      */
    886     private void loadInvitableAccountTypes(Result contactData) {
    887         final ArrayList<AccountType> resultList = Lists.newArrayList();
    888         if (!contactData.isUserProfile()) {
    889             Map<AccountTypeWithDataSet, AccountType> invitables =
    890                     AccountTypeManager.getInstance(getContext()).getUsableInvitableAccountTypes();
    891             if (!invitables.isEmpty()) {
    892                 final Map<AccountTypeWithDataSet, AccountType> resultMap =
    893                         Maps.newHashMap(invitables);
    894 
    895                 // Remove the ones that already have a raw contact in the current contact
    896                 for (Entity entity : contactData.getEntities()) {
    897                     final ContentValues values = entity.getEntityValues();
    898                     final AccountTypeWithDataSet type = AccountTypeWithDataSet.get(
    899                             values.getAsString(RawContacts.ACCOUNT_TYPE),
    900                             values.getAsString(RawContacts.DATA_SET));
    901                     resultMap.remove(type);
    902                 }
    903 
    904                 resultList.addAll(resultMap.values());
    905             }
    906         }
    907 
    908         // Set to mInvitableAccountTypes
    909         contactData.mInvitableAccountTypes = resultList;
    910     }
    911 
    912     /**
    913      * Extracts Contact level columns from the cursor.
    914      */
    915     private Result loadContactHeaderData(final Cursor cursor, Uri contactUri) {
    916         final String directoryParameter =
    917                 contactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
    918         final long directoryId = directoryParameter == null
    919                 ? Directory.DEFAULT
    920                 : Long.parseLong(directoryParameter);
    921         final long contactId = cursor.getLong(ContactQuery.CONTACT_ID);
    922         final String lookupKey = cursor.getString(ContactQuery.LOOKUP_KEY);
    923         final long nameRawContactId = cursor.getLong(ContactQuery.NAME_RAW_CONTACT_ID);
    924         final int displayNameSource = cursor.getInt(ContactQuery.DISPLAY_NAME_SOURCE);
    925         final String displayName = cursor.getString(ContactQuery.DISPLAY_NAME);
    926         final String altDisplayName = cursor.getString(ContactQuery.ALT_DISPLAY_NAME);
    927         final String phoneticName = cursor.getString(ContactQuery.PHONETIC_NAME);
    928         final long photoId = cursor.getLong(ContactQuery.PHOTO_ID);
    929         final String photoUri = cursor.getString(ContactQuery.PHOTO_URI);
    930         final boolean starred = cursor.getInt(ContactQuery.STARRED) != 0;
    931         final Integer presence = cursor.isNull(ContactQuery.CONTACT_PRESENCE)
    932                 ? null
    933                 : cursor.getInt(ContactQuery.CONTACT_PRESENCE);
    934         final boolean sendToVoicemail = cursor.getInt(ContactQuery.SEND_TO_VOICEMAIL) == 1;
    935         final String customRingtone = cursor.getString(ContactQuery.CUSTOM_RINGTONE);
    936         final boolean isUserProfile = cursor.getInt(ContactQuery.IS_USER_PROFILE) == 1;
    937 
    938         Uri lookupUri;
    939         if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) {
    940             lookupUri = ContentUris.withAppendedId(
    941                 Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), contactId);
    942         } else {
    943             lookupUri = contactUri;
    944         }
    945 
    946         return new Result(mRequestedUri, contactUri, lookupUri, directoryId, lookupKey,
    947                 contactId, nameRawContactId, displayNameSource, photoId, photoUri, displayName,
    948                 altDisplayName, phoneticName, starred, presence, sendToVoicemail,
    949                 customRingtone, isUserProfile);
    950     }
    951 
    952     /**
    953      * Extracts RawContact level columns from the cursor.
    954      */
    955     private ContentValues loadRawContact(Cursor cursor) {
    956         ContentValues cv = new ContentValues();
    957 
    958         cv.put(RawContacts._ID, cursor.getLong(ContactQuery.RAW_CONTACT_ID));
    959 
    960         cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_NAME);
    961         cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE);
    962         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SET);
    963         cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE_AND_DATA_SET);
    964         cursorColumnToContentValues(cursor, cv, ContactQuery.DIRTY);
    965         cursorColumnToContentValues(cursor, cv, ContactQuery.VERSION);
    966         cursorColumnToContentValues(cursor, cv, ContactQuery.SOURCE_ID);
    967         cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC1);
    968         cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC2);
    969         cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC3);
    970         cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC4);
    971         cursorColumnToContentValues(cursor, cv, ContactQuery.DELETED);
    972         cursorColumnToContentValues(cursor, cv, ContactQuery.CONTACT_ID);
    973         cursorColumnToContentValues(cursor, cv, ContactQuery.STARRED);
    974         cursorColumnToContentValues(cursor, cv, ContactQuery.NAME_VERIFIED);
    975 
    976         return cv;
    977     }
    978 
    979     /**
    980      * Extracts Data level columns from the cursor.
    981      */
    982     private ContentValues loadData(Cursor cursor) {
    983         ContentValues cv = new ContentValues();
    984 
    985         cv.put(Data._ID, cursor.getLong(ContactQuery.DATA_ID));
    986 
    987         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA1);
    988         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA2);
    989         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA3);
    990         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA4);
    991         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA5);
    992         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA6);
    993         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA7);
    994         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA8);
    995         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA9);
    996         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA10);
    997         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA11);
    998         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA12);
    999         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA13);
   1000         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA14);
   1001         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA15);
   1002         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC1);
   1003         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC2);
   1004         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC3);
   1005         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC4);
   1006         cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_VERSION);
   1007         cursorColumnToContentValues(cursor, cv, ContactQuery.IS_PRIMARY);
   1008         cursorColumnToContentValues(cursor, cv, ContactQuery.IS_SUPERPRIMARY);
   1009         cursorColumnToContentValues(cursor, cv, ContactQuery.MIMETYPE);
   1010         cursorColumnToContentValues(cursor, cv, ContactQuery.RES_PACKAGE);
   1011         cursorColumnToContentValues(cursor, cv, ContactQuery.GROUP_SOURCE_ID);
   1012         cursorColumnToContentValues(cursor, cv, ContactQuery.CHAT_CAPABILITY);
   1013 
   1014         return cv;
   1015     }
   1016 
   1017     private void cursorColumnToContentValues(
   1018             Cursor cursor, ContentValues values, int index) {
   1019         switch (cursor.getType(index)) {
   1020             case Cursor.FIELD_TYPE_NULL:
   1021                 // don't put anything in the content values
   1022                 break;
   1023             case Cursor.FIELD_TYPE_INTEGER:
   1024                 values.put(ContactQuery.COLUMNS[index], cursor.getLong(index));
   1025                 break;
   1026             case Cursor.FIELD_TYPE_STRING:
   1027                 values.put(ContactQuery.COLUMNS[index], cursor.getString(index));
   1028                 break;
   1029             case Cursor.FIELD_TYPE_BLOB:
   1030                 values.put(ContactQuery.COLUMNS[index], cursor.getBlob(index));
   1031                 break;
   1032             default:
   1033                 throw new IllegalStateException("Invalid or unhandled data type");
   1034         }
   1035     }
   1036 
   1037     private void loadDirectoryMetaData(Result result) {
   1038         long directoryId = result.getDirectoryId();
   1039 
   1040         Cursor cursor = getContext().getContentResolver().query(
   1041                 ContentUris.withAppendedId(Directory.CONTENT_URI, directoryId),
   1042                 DirectoryQuery.COLUMNS, null, null, null);
   1043         if (cursor == null) {
   1044             return;
   1045         }
   1046         try {
   1047             if (cursor.moveToFirst()) {
   1048                 final String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME);
   1049                 final String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME);
   1050                 final int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID);
   1051                 final String accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
   1052                 final String accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
   1053                 final int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT);
   1054                 String directoryType = null;
   1055                 if (!TextUtils.isEmpty(packageName)) {
   1056                     PackageManager pm = getContext().getPackageManager();
   1057                     try {
   1058                         Resources resources = pm.getResourcesForApplication(packageName);
   1059                         directoryType = resources.getString(typeResourceId);
   1060                     } catch (NameNotFoundException e) {
   1061                         Log.w(TAG, "Contact directory resource not found: "
   1062                                 + packageName + "." + typeResourceId);
   1063                     }
   1064                 }
   1065 
   1066                 result.setDirectoryMetaData(
   1067                         displayName, directoryType, accountType, accountName, exportSupport);
   1068             }
   1069         } finally {
   1070             cursor.close();
   1071         }
   1072     }
   1073 
   1074     /**
   1075      * Loads groups meta-data for all groups associated with all constituent raw contacts'
   1076      * accounts.
   1077      */
   1078     private void loadGroupMetaData(Result result) {
   1079         StringBuilder selection = new StringBuilder();
   1080         ArrayList<String> selectionArgs = new ArrayList<String>();
   1081         for (Entity entity : result.mEntities) {
   1082             ContentValues values = entity.getEntityValues();
   1083             String accountName = values.getAsString(RawContacts.ACCOUNT_NAME);
   1084             String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
   1085             String dataSet = values.getAsString(RawContacts.DATA_SET);
   1086             if (accountName != null && accountType != null) {
   1087                 if (selection.length() != 0) {
   1088                     selection.append(" OR ");
   1089                 }
   1090                 selection.append(
   1091                         "(" + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?");
   1092                 selectionArgs.add(accountName);
   1093                 selectionArgs.add(accountType);
   1094 
   1095                 if (dataSet != null) {
   1096                     selection.append(" AND " + Groups.DATA_SET + "=?");
   1097                     selectionArgs.add(dataSet);
   1098                 } else {
   1099                     selection.append(" AND " + Groups.DATA_SET + " IS NULL");
   1100                 }
   1101                 selection.append(")");
   1102             }
   1103         }
   1104         final ArrayList<GroupMetaData> groupList = new ArrayList<GroupMetaData>();
   1105         final Cursor cursor = getContext().getContentResolver().query(Groups.CONTENT_URI,
   1106                 GroupQuery.COLUMNS, selection.toString(), selectionArgs.toArray(new String[0]),
   1107                 null);
   1108         try {
   1109             while (cursor.moveToNext()) {
   1110                 final String accountName = cursor.getString(GroupQuery.ACCOUNT_NAME);
   1111                 final String accountType = cursor.getString(GroupQuery.ACCOUNT_TYPE);
   1112                 final String dataSet = cursor.getString(GroupQuery.DATA_SET);
   1113                 final long groupId = cursor.getLong(GroupQuery.ID);
   1114                 final String title = cursor.getString(GroupQuery.TITLE);
   1115                 final boolean defaultGroup = cursor.isNull(GroupQuery.AUTO_ADD)
   1116                         ? false
   1117                         : cursor.getInt(GroupQuery.AUTO_ADD) != 0;
   1118                 final boolean favorites = cursor.isNull(GroupQuery.FAVORITES)
   1119                         ? false
   1120                         : cursor.getInt(GroupQuery.FAVORITES) != 0;
   1121 
   1122                 groupList.add(new GroupMetaData(
   1123                         accountName, accountType, dataSet, groupId, title, defaultGroup,
   1124                         favorites));
   1125             }
   1126         } finally {
   1127             cursor.close();
   1128         }
   1129         result.mGroups = groupList;
   1130     }
   1131 
   1132     /**
   1133      * Loads all stream items and stream item photos belonging to this contact.
   1134      */
   1135     private void loadStreamItems(Result result) {
   1136         Cursor cursor = getContext().getContentResolver().query(
   1137                 Contacts.CONTENT_LOOKUP_URI.buildUpon()
   1138                         .appendPath(result.getLookupKey())
   1139                         .appendPath(Contacts.StreamItems.CONTENT_DIRECTORY).build(),
   1140                 null, null, null, null);
   1141         LongSparseArray<StreamItemEntry> streamItemsById =
   1142                 new LongSparseArray<StreamItemEntry>();
   1143         ArrayList<StreamItemEntry> streamItems = new ArrayList<StreamItemEntry>();
   1144         try {
   1145             while (cursor.moveToNext()) {
   1146                 StreamItemEntry streamItem = new StreamItemEntry(cursor);
   1147                 streamItemsById.put(streamItem.getId(), streamItem);
   1148                 streamItems.add(streamItem);
   1149             }
   1150         } finally {
   1151             cursor.close();
   1152         }
   1153 
   1154         // Pre-decode all HTMLs
   1155         final long start = System.currentTimeMillis();
   1156         for (StreamItemEntry streamItem : streamItems) {
   1157             streamItem.decodeHtml(getContext());
   1158         }
   1159         final long end = System.currentTimeMillis();
   1160         if (DEBUG) {
   1161             Log.d(TAG, "Decoded HTML for " + streamItems.size() + " items, took "
   1162                     + (end - start) + " ms");
   1163         }
   1164 
   1165         // Now retrieve any photo records associated with the stream items.
   1166         if (!streamItems.isEmpty()) {
   1167             if (result.isUserProfile()) {
   1168                 // If the stream items we're loading are for the profile, we can't bulk-load the
   1169                 // stream items with a custom selection.
   1170                 for (StreamItemEntry entry : streamItems) {
   1171                     Cursor siCursor = getContext().getContentResolver().query(
   1172                             Uri.withAppendedPath(
   1173                                     ContentUris.withAppendedId(
   1174                                             StreamItems.CONTENT_URI, entry.getId()),
   1175                                     StreamItems.StreamItemPhotos.CONTENT_DIRECTORY),
   1176                             null, null, null, null);
   1177                     try {
   1178                         while (siCursor.moveToNext()) {
   1179                             entry.addPhoto(new StreamItemPhotoEntry(siCursor));
   1180                         }
   1181                     } finally {
   1182                         siCursor.close();
   1183                     }
   1184                 }
   1185             } else {
   1186                 String[] streamItemIdArr = new String[streamItems.size()];
   1187                 StringBuilder streamItemPhotoSelection = new StringBuilder();
   1188                 streamItemPhotoSelection.append(StreamItemPhotos.STREAM_ITEM_ID + " IN (");
   1189                 for (int i = 0; i < streamItems.size(); i++) {
   1190                     if (i > 0) {
   1191                         streamItemPhotoSelection.append(",");
   1192                     }
   1193                     streamItemPhotoSelection.append("?");
   1194                     streamItemIdArr[i] = String.valueOf(streamItems.get(i).getId());
   1195                 }
   1196                 streamItemPhotoSelection.append(")");
   1197                 Cursor sipCursor = getContext().getContentResolver().query(
   1198                         StreamItems.CONTENT_PHOTO_URI,
   1199                         null, streamItemPhotoSelection.toString(), streamItemIdArr,
   1200                         StreamItemPhotos.STREAM_ITEM_ID);
   1201                 try {
   1202                     while (sipCursor.moveToNext()) {
   1203                         long streamItemId = sipCursor.getLong(
   1204                                 sipCursor.getColumnIndex(StreamItemPhotos.STREAM_ITEM_ID));
   1205                         StreamItemEntry streamItem = streamItemsById.get(streamItemId);
   1206                         streamItem.addPhoto(new StreamItemPhotoEntry(sipCursor));
   1207                     }
   1208                 } finally {
   1209                     sipCursor.close();
   1210                 }
   1211             }
   1212         }
   1213 
   1214         // Set the sorted stream items on the result.
   1215         Collections.sort(streamItems);
   1216         result.mStreamItems = streamItems;
   1217     }
   1218 
   1219     @Override
   1220     public void deliverResult(Result result) {
   1221         unregisterObserver();
   1222 
   1223         // The creator isn't interested in any further updates
   1224         if (isReset() || result == null) {
   1225             return;
   1226         }
   1227 
   1228         mContact = result;
   1229 
   1230         if (result.isLoaded()) {
   1231             mLookupUri = result.getLookupUri();
   1232 
   1233             if (!result.isDirectoryEntry()) {
   1234                 Log.i(TAG, "Registering content observer for " + mLookupUri);
   1235                 if (mObserver == null) {
   1236                     mObserver = new ForceLoadContentObserver();
   1237                 }
   1238                 getContext().getContentResolver().registerContentObserver(
   1239                         mLookupUri, true, mObserver);
   1240             }
   1241 
   1242             if (mPostViewNotification) {
   1243                 // inform the source of the data that this contact is being looked at
   1244                 postViewNotificationToSyncAdapter();
   1245             }
   1246         }
   1247 
   1248         super.deliverResult(mContact);
   1249     }
   1250 
   1251     /**
   1252      * Posts a message to the contributing sync adapters that have opted-in, notifying them
   1253      * that the contact has just been loaded
   1254      */
   1255     private void postViewNotificationToSyncAdapter() {
   1256         Context context = getContext();
   1257         for (Entity entity : mContact.getEntities()) {
   1258             final ContentValues entityValues = entity.getEntityValues();
   1259             final long rawContactId = entityValues.getAsLong(RawContacts.Entity._ID);
   1260             if (mNotifiedRawContactIds.contains(rawContactId)) {
   1261                 continue; // Already notified for this raw contact.
   1262             }
   1263             mNotifiedRawContactIds.add(rawContactId);
   1264             final String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
   1265             final String dataSet = entityValues.getAsString(RawContacts.DATA_SET);
   1266             final AccountType accountType = AccountTypeManager.getInstance(context).getAccountType(
   1267                     type, dataSet);
   1268             final String serviceName = accountType.getViewContactNotifyServiceClassName();
   1269             final String servicePackageName = accountType.getViewContactNotifyServicePackageName();
   1270             if (!TextUtils.isEmpty(serviceName) && !TextUtils.isEmpty(servicePackageName)) {
   1271                 final Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
   1272                 final Intent intent = new Intent();
   1273                 intent.setClassName(servicePackageName, serviceName);
   1274                 intent.setAction(Intent.ACTION_VIEW);
   1275                 intent.setDataAndType(uri, RawContacts.CONTENT_ITEM_TYPE);
   1276                 try {
   1277                     context.startService(intent);
   1278                 } catch (Exception e) {
   1279                     Log.e(TAG, "Error sending message to source-app", e);
   1280                 }
   1281             }
   1282         }
   1283     }
   1284 
   1285     private void unregisterObserver() {
   1286         if (mObserver != null) {
   1287             getContext().getContentResolver().unregisterContentObserver(mObserver);
   1288             mObserver = null;
   1289         }
   1290     }
   1291 
   1292     /**
   1293      * Sets whether to load stream items. Will trigger a reload if the value has changed.
   1294      * At the moment, this is only used for debugging purposes
   1295      */
   1296     public void setLoadStreamItems(boolean value) {
   1297         if (mLoadStreamItems != value) {
   1298             mLoadStreamItems = value;
   1299             onContentChanged();
   1300         }
   1301     }
   1302 
   1303     /**
   1304      * Fully upgrades this ContactLoader to one with all lists fully loaded. When done, the
   1305      * new result will be delivered
   1306      */
   1307     public void upgradeToFullContact() {
   1308         // Everything requested already? Nothing to do, so let's bail out
   1309         if (mLoadGroupMetaData && mLoadInvitableAccountTypes && mLoadStreamItems
   1310                 && mPostViewNotification) return;
   1311 
   1312         mLoadGroupMetaData = true;
   1313         mLoadInvitableAccountTypes = true;
   1314         mLoadStreamItems = true;
   1315         mPostViewNotification = true;
   1316 
   1317         // Cache the current result, so that we only load the "missing" parts of the contact.
   1318         cacheResult();
   1319 
   1320         // Our load parameters have changed, so let's pretend the data has changed. Its the same
   1321         // thing, essentially.
   1322         onContentChanged();
   1323     }
   1324 
   1325     public boolean getLoadStreamItems() {
   1326         return mLoadStreamItems;
   1327     }
   1328 
   1329     public Uri getLookupUri() {
   1330         return mLookupUri;
   1331     }
   1332 
   1333     @Override
   1334     protected void onStartLoading() {
   1335         if (mContact != null) {
   1336             deliverResult(mContact);
   1337         }
   1338 
   1339         if (takeContentChanged() || mContact == null) {
   1340             forceLoad();
   1341         }
   1342     }
   1343 
   1344     @Override
   1345     protected void onStopLoading() {
   1346         cancelLoad();
   1347     }
   1348 
   1349     @Override
   1350     protected void onReset() {
   1351         super.onReset();
   1352         cancelLoad();
   1353         unregisterObserver();
   1354         mContact = null;
   1355     }
   1356 
   1357     /**
   1358      * Caches the result, which is useful when we switch from activity to activity, using the same
   1359      * contact. If the next load is for a different contact, the cached result will be dropped
   1360      */
   1361     public void cacheResult() {
   1362         if (mContact == null || !mContact.isLoaded()) {
   1363             sCachedResult = null;
   1364         } else {
   1365             sCachedResult = mContact;
   1366         }
   1367     }
   1368 }
   1369