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