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