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