com.android.contacts.common.model.ContactLoader.java Source code

Java tutorial

Introduction

Here is the source code for com.android.contacts.common.model.ContactLoader.java

Source

/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */

package com.android.contacts.common.model;

import android.content.AsyncTaskLoader;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Directory;
import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.RawContacts;
import android.text.TextUtils;
import android.util.Log;

import com.android.contacts.common.GeoUtil;
import com.android.contacts.common.GroupMetaData;
import com.android.contacts.common.model.account.AccountType;
import com.android.contacts.common.model.account.AccountTypeWithDataSet;
import com.android.contacts.common.util.Constants;
import com.android.contacts.common.util.ContactLoaderUtils;
import com.android.contacts.common.util.DataStatus;
import com.android.contacts.common.util.UriUtils;
import com.android.contacts.common.model.dataitem.DataItem;
import com.android.contacts.common.model.dataitem.PhoneDataItem;
import com.android.contacts.common.model.dataitem.PhotoDataItem;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
 * Loads a single Contact and all it constituent RawContacts.
 */
public class ContactLoader extends AsyncTaskLoader<Contact> {

    private static final String TAG = ContactLoader.class.getSimpleName();

    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    /** A short-lived cache that can be set by {@link #cacheResult()} */
    private static Contact sCachedResult = null;

    private final Uri mRequestedUri;
    private Uri mLookupUri;
    private boolean mLoadGroupMetaData;
    private boolean mLoadInvitableAccountTypes;
    private boolean mPostViewNotification;
    private boolean mComputeFormattedPhoneNumber;
    private Contact mContact;
    private ForceLoadContentObserver mObserver;
    private final Set<Long> mNotifiedRawContactIds = Sets.newHashSet();

    public ContactLoader(Context context, Uri lookupUri, boolean postViewNotification) {
        this(context, lookupUri, false, false, postViewNotification, false);
    }

    public ContactLoader(Context context, Uri lookupUri, boolean loadGroupMetaData,
            boolean loadInvitableAccountTypes, boolean postViewNotification, boolean computeFormattedPhoneNumber) {
        super(context);
        mLookupUri = lookupUri;
        mRequestedUri = lookupUri;
        mLoadGroupMetaData = loadGroupMetaData;
        mLoadInvitableAccountTypes = loadInvitableAccountTypes;
        mPostViewNotification = postViewNotification;
        mComputeFormattedPhoneNumber = computeFormattedPhoneNumber;
    }

    /**
     * Projection used for the query that loads all data for the entire contact (except for
     * social stream items).
     */
    private static class ContactQuery {
        static final String[] COLUMNS = new String[] { Contacts.NAME_RAW_CONTACT_ID, Contacts.DISPLAY_NAME_SOURCE,
                Contacts.LOOKUP_KEY, Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME_ALTERNATIVE,
                Contacts.PHONETIC_NAME, Contacts.PHOTO_ID, Contacts.STARRED, Contacts.CONTACT_PRESENCE,
                Contacts.CONTACT_STATUS, Contacts.CONTACT_STATUS_TIMESTAMP, Contacts.CONTACT_STATUS_RES_PACKAGE,
                Contacts.CONTACT_STATUS_LABEL, Contacts.Entity.CONTACT_ID, Contacts.Entity.RAW_CONTACT_ID,

                RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_TYPE, RawContacts.DATA_SET, RawContacts.DIRTY,
                RawContacts.VERSION, RawContacts.SOURCE_ID, RawContacts.SYNC1, RawContacts.SYNC2, RawContacts.SYNC3,
                RawContacts.SYNC4, RawContacts.DELETED,

                Contacts.Entity.DATA_ID, Data.DATA1, Data.DATA2, Data.DATA3, Data.DATA4, Data.DATA5, Data.DATA6,
                Data.DATA7, Data.DATA8, Data.DATA9, Data.DATA10, Data.DATA11, Data.DATA12, Data.DATA13, Data.DATA14,
                Data.DATA15, Data.SYNC1, Data.SYNC2, Data.SYNC3, Data.SYNC4, Data.DATA_VERSION, Data.IS_PRIMARY,
                Data.IS_SUPER_PRIMARY, Data.MIMETYPE,

                GroupMembership.GROUP_SOURCE_ID,

                Data.PRESENCE, Data.CHAT_CAPABILITY, Data.STATUS, Data.STATUS_RES_PACKAGE, Data.STATUS_ICON,
                Data.STATUS_LABEL, Data.STATUS_TIMESTAMP,

                Contacts.PHOTO_URI, Contacts.SEND_TO_VOICEMAIL, Contacts.CUSTOM_RINGTONE, Contacts.IS_USER_PROFILE,

                Data.TIMES_USED, Data.LAST_TIME_USED, };

        public static final int NAME_RAW_CONTACT_ID = 0;
        public static final int DISPLAY_NAME_SOURCE = 1;
        public static final int LOOKUP_KEY = 2;
        public static final int DISPLAY_NAME = 3;
        public static final int ALT_DISPLAY_NAME = 4;
        public static final int PHONETIC_NAME = 5;
        public static final int PHOTO_ID = 6;
        public static final int STARRED = 7;
        public static final int CONTACT_PRESENCE = 8;
        public static final int CONTACT_STATUS = 9;
        public static final int CONTACT_STATUS_TIMESTAMP = 10;
        public static final int CONTACT_STATUS_RES_PACKAGE = 11;
        public static final int CONTACT_STATUS_LABEL = 12;
        public static final int CONTACT_ID = 13;
        public static final int RAW_CONTACT_ID = 14;

        public static final int ACCOUNT_NAME = 15;
        public static final int ACCOUNT_TYPE = 16;
        public static final int DATA_SET = 17;
        public static final int DIRTY = 18;
        public static final int VERSION = 19;
        public static final int SOURCE_ID = 20;
        public static final int SYNC1 = 21;
        public static final int SYNC2 = 22;
        public static final int SYNC3 = 23;
        public static final int SYNC4 = 24;
        public static final int DELETED = 25;

        public static final int DATA_ID = 26;
        public static final int DATA1 = 27;
        public static final int DATA2 = 28;
        public static final int DATA3 = 29;
        public static final int DATA4 = 30;
        public static final int DATA5 = 31;
        public static final int DATA6 = 32;
        public static final int DATA7 = 33;
        public static final int DATA8 = 34;
        public static final int DATA9 = 35;
        public static final int DATA10 = 36;
        public static final int DATA11 = 37;
        public static final int DATA12 = 38;
        public static final int DATA13 = 39;
        public static final int DATA14 = 40;
        public static final int DATA15 = 41;
        public static final int DATA_SYNC1 = 42;
        public static final int DATA_SYNC2 = 43;
        public static final int DATA_SYNC3 = 44;
        public static final int DATA_SYNC4 = 45;
        public static final int DATA_VERSION = 46;
        public static final int IS_PRIMARY = 47;
        public static final int IS_SUPERPRIMARY = 48;
        public static final int MIMETYPE = 49;

        public static final int GROUP_SOURCE_ID = 50;

        public static final int PRESENCE = 51;
        public static final int CHAT_CAPABILITY = 52;
        public static final int STATUS = 53;
        public static final int STATUS_RES_PACKAGE = 54;
        public static final int STATUS_ICON = 55;
        public static final int STATUS_LABEL = 56;
        public static final int STATUS_TIMESTAMP = 57;

        public static final int PHOTO_URI = 58;
        public static final int SEND_TO_VOICEMAIL = 59;
        public static final int CUSTOM_RINGTONE = 60;
        public static final int IS_USER_PROFILE = 61;

        public static final int TIMES_USED = 62;
        public static final int LAST_TIME_USED = 63;
    }

    /**
     * Projection used for the query that loads all data for the entire contact.
     */
    private static class DirectoryQuery {
        static final String[] COLUMNS = new String[] { Directory.DISPLAY_NAME, Directory.PACKAGE_NAME,
                Directory.TYPE_RESOURCE_ID, Directory.ACCOUNT_TYPE, Directory.ACCOUNT_NAME,
                Directory.EXPORT_SUPPORT, };

        public static final int DISPLAY_NAME = 0;
        public static final int PACKAGE_NAME = 1;
        public static final int TYPE_RESOURCE_ID = 2;
        public static final int ACCOUNT_TYPE = 3;
        public static final int ACCOUNT_NAME = 4;
        public static final int EXPORT_SUPPORT = 5;
    }

    private static class GroupQuery {
        static final String[] COLUMNS = new String[] { Groups.ACCOUNT_NAME, Groups.ACCOUNT_TYPE, Groups.DATA_SET,
                Groups._ID, Groups.TITLE, Groups.AUTO_ADD, Groups.FAVORITES, };

        public static final int ACCOUNT_NAME = 0;
        public static final int ACCOUNT_TYPE = 1;
        public static final int DATA_SET = 2;
        public static final int ID = 3;
        public static final int TITLE = 4;
        public static final int AUTO_ADD = 5;
        public static final int FAVORITES = 6;
    }

    @Override
    public Contact loadInBackground() {
        try {
            final ContentResolver resolver = getContext().getContentResolver();
            final Uri uriCurrentFormat = ContactLoaderUtils.ensureIsContactUri(resolver, mLookupUri);
            final Contact cachedResult = sCachedResult;
            sCachedResult = null;
            // Is this the same Uri as what we had before already? In that case, reuse that result
            final Contact result;
            final boolean resultIsCached;
            if (cachedResult != null && UriUtils.areEqual(cachedResult.getLookupUri(), mLookupUri)) {
                // We are using a cached result from earlier. Below, we should make sure
                // we are not doing any more network or disc accesses
                result = new Contact(mRequestedUri, cachedResult);
                resultIsCached = true;
            } else {
                if (uriCurrentFormat.getLastPathSegment().equals(Constants.LOOKUP_URI_ENCODED)) {
                    result = loadEncodedContactEntity(uriCurrentFormat, mLookupUri);
                } else {
                    result = loadContactEntity(resolver, uriCurrentFormat);
                }
                resultIsCached = false;
            }
            if (result.isLoaded()) {
                if (result.isDirectoryEntry()) {
                    if (!resultIsCached) {
                        loadDirectoryMetaData(result);
                    }
                } else if (mLoadGroupMetaData) {
                    if (result.getGroupMetaData() == null) {
                        loadGroupMetaData(result);
                    }
                }
                if (mComputeFormattedPhoneNumber) {
                    computeFormattedPhoneNumbers(result);
                }
                if (!resultIsCached)
                    loadPhotoBinaryData(result);

                // Note ME profile should never have "Add connection"
                if (mLoadInvitableAccountTypes && result.getInvitableAccountTypes() == null) {
                    loadInvitableAccountTypes(result);
                }
            }
            return result;
        } catch (Exception e) {
            Log.e(TAG, "Error loading the contact: " + mLookupUri, e);
            return Contact.forError(mRequestedUri, e);
        }
    }

    /**
     * Parses a {@link Contact} stored as a JSON string in a lookup URI.
     *
     * @param lookupUri The contact information to parse .
     * @return The parsed {@code Contact} information.
     * @throws JSONException
     */
    public static Contact parseEncodedContactEntity(Uri lookupUri) {
        try {
            return loadEncodedContactEntity(lookupUri, lookupUri);
        } catch (JSONException je) {
            return null;
        }
    }

    private static Contact loadEncodedContactEntity(Uri uri, Uri lookupUri) throws JSONException {
        final String jsonString = uri.getEncodedFragment();
        final JSONObject json = new JSONObject(jsonString);

        final long directoryId = Long.valueOf(uri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY));

        final String displayName = json.optString(Contacts.DISPLAY_NAME);
        final String altDisplayName = json.optString(Contacts.DISPLAY_NAME_ALTERNATIVE, displayName);
        final int displayNameSource = json.getInt(Contacts.DISPLAY_NAME_SOURCE);
        final String photoUri = json.optString(Contacts.PHOTO_URI, null);
        final Contact contact = new Contact(uri, uri, lookupUri, directoryId, null /* lookupKey */, -1 /* id */,
                -1 /* nameRawContactId */, displayNameSource, 0 /* photoId */, photoUri, displayName,
                altDisplayName, null /* phoneticName */, false /* starred */, null /* presence */,
                false /* sendToVoicemail */, null /* customRingtone */, false /* isUserProfile */);

        contact.setStatuses(new ImmutableMap.Builder<Long, DataStatus>().build());

        final String accountName = json.optString(RawContacts.ACCOUNT_NAME, null);
        final String directoryName = uri.getQueryParameter(Directory.DISPLAY_NAME);
        if (accountName != null) {
            final String accountType = json.getString(RawContacts.ACCOUNT_TYPE);
            contact.setDirectoryMetaData(directoryName, null, accountName, accountType,
                    json.optInt(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY));
        } else {
            contact.setDirectoryMetaData(directoryName, null, null, null,
                    json.optInt(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_ANY_ACCOUNT));
        }

        final ContentValues values = new ContentValues();
        values.put(Data._ID, -1);
        values.put(Data.CONTACT_ID, -1);
        final RawContact rawContact = new RawContact(values);

        final JSONObject items = json.getJSONObject(Contacts.CONTENT_ITEM_TYPE);
        final Iterator keys = items.keys();
        while (keys.hasNext()) {
            final String mimetype = (String) keys.next();

            // Could be single object or array.
            final JSONObject obj = items.optJSONObject(mimetype);
            if (obj == null) {
                final JSONArray array = items.getJSONArray(mimetype);
                for (int i = 0; i < array.length(); i++) {
                    final JSONObject item = array.getJSONObject(i);
                    processOneRecord(rawContact, item, mimetype);
                }
            } else {
                processOneRecord(rawContact, obj, mimetype);
            }
        }

        contact.setRawContacts(new ImmutableList.Builder<RawContact>().add(rawContact).build());
        return contact;
    }

    private static void processOneRecord(RawContact rawContact, JSONObject item, String mimetype)
            throws JSONException {
        final ContentValues itemValues = new ContentValues();
        itemValues.put(Data.MIMETYPE, mimetype);
        itemValues.put(Data._ID, -1);

        final Iterator iterator = item.keys();
        while (iterator.hasNext()) {
            String name = (String) iterator.next();
            final Object o = item.get(name);
            if (o instanceof String) {
                itemValues.put(name, (String) o);
            } else if (o instanceof Integer) {
                itemValues.put(name, (Integer) o);
            }
        }
        rawContact.addDataItemValues(itemValues);
    }

    private Contact loadContactEntity(ContentResolver resolver, Uri contactUri) {
        Uri entityUri = Uri.withAppendedPath(contactUri, Contacts.Entity.CONTENT_DIRECTORY);
        Cursor cursor = resolver.query(entityUri, ContactQuery.COLUMNS, null, null, Contacts.Entity.RAW_CONTACT_ID);
        if (cursor == null) {
            Log.e(TAG, "No cursor returned in loadContactEntity");
            return Contact.forNotFound(mRequestedUri);
        }

        try {
            if (!cursor.moveToFirst()) {
                cursor.close();
                return Contact.forNotFound(mRequestedUri);
            }

            // Create the loaded contact starting with the header data.
            Contact contact = loadContactHeaderData(cursor, contactUri);

            // Fill in the raw contacts, which is wrapped in an Entity and any
            // status data.  Initially, result has empty entities and statuses.
            long currentRawContactId = -1;
            RawContact rawContact = null;
            ImmutableList.Builder<RawContact> rawContactsBuilder = new ImmutableList.Builder<RawContact>();
            ImmutableMap.Builder<Long, DataStatus> statusesBuilder = new ImmutableMap.Builder<Long, DataStatus>();
            do {
                long rawContactId = cursor.getLong(ContactQuery.RAW_CONTACT_ID);
                if (rawContactId != currentRawContactId) {
                    // First time to see this raw contact id, so create a new entity, and
                    // add it to the result's entities.
                    currentRawContactId = rawContactId;
                    rawContact = new RawContact(loadRawContactValues(cursor));
                    rawContactsBuilder.add(rawContact);
                }
                if (!cursor.isNull(ContactQuery.DATA_ID)) {
                    ContentValues data = loadDataValues(cursor);
                    rawContact.addDataItemValues(data);

                    if (!cursor.isNull(ContactQuery.PRESENCE) || !cursor.isNull(ContactQuery.STATUS)) {
                        final DataStatus status = new DataStatus(cursor);
                        final long dataId = cursor.getLong(ContactQuery.DATA_ID);
                        statusesBuilder.put(dataId, status);
                    }
                }
            } while (cursor.moveToNext());

            contact.setRawContacts(rawContactsBuilder.build());
            contact.setStatuses(statusesBuilder.build());

            return contact;
        } finally {
            cursor.close();
        }
    }

    /**
     * Looks for the photo data item in entities. If found, a thumbnail will be stored. A larger
     * photo will also be stored if available.
     */
    private void loadPhotoBinaryData(Contact contactData) {
        loadThumbnailBinaryData(contactData);

        // Try to load the large photo from a file using the photo URI.
        String photoUri = contactData.getPhotoUri();
        if (photoUri != null) {
            try {
                final InputStream inputStream;
                final AssetFileDescriptor fd;
                final Uri uri = Uri.parse(photoUri);
                final String scheme = uri.getScheme();
                if ("http".equals(scheme) || "https".equals(scheme)) {
                    // Support HTTP urls that might come from extended directories
                    inputStream = new URL(photoUri).openStream();
                    fd = null;
                } else {
                    fd = getContext().getContentResolver().openAssetFileDescriptor(uri, "r");
                    inputStream = fd.createInputStream();
                }
                byte[] buffer = new byte[16 * 1024];
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                try {
                    int size;
                    while ((size = inputStream.read(buffer)) != -1) {
                        baos.write(buffer, 0, size);
                    }
                    contactData.setPhotoBinaryData(baos.toByteArray());
                } finally {
                    inputStream.close();
                    if (fd != null) {
                        fd.close();
                    }
                }
                return;
            } catch (IOException ioe) {
                // Just fall back to the case below.
            }
        }

        // If we couldn't load from a file, fall back to the data blob.
        contactData.setPhotoBinaryData(contactData.getThumbnailPhotoBinaryData());
    }

    private void loadThumbnailBinaryData(Contact contactData) {
        final long photoId = contactData.getPhotoId();
        if (photoId <= 0) {
            // No photo ID
            return;
        }

        for (RawContact rawContact : contactData.getRawContacts()) {
            for (DataItem dataItem : rawContact.getDataItems()) {
                if (dataItem.getId() == photoId) {
                    if (!(dataItem instanceof PhotoDataItem)) {
                        break;
                    }

                    final PhotoDataItem photo = (PhotoDataItem) dataItem;
                    contactData.setThumbnailPhotoBinaryData(photo.getPhoto());
                    break;
                }
            }
        }
    }

    /**
     * Sets the "invitable" account types to {@link Contact#mInvitableAccountTypes}.
     */
    private void loadInvitableAccountTypes(Contact contactData) {
        final ImmutableList.Builder<AccountType> resultListBuilder = new ImmutableList.Builder<AccountType>();
        if (!contactData.isUserProfile()) {
            Map<AccountTypeWithDataSet, AccountType> invitables = AccountTypeManager.getInstance(getContext())
                    .getUsableInvitableAccountTypes();
            if (!invitables.isEmpty()) {
                final Map<AccountTypeWithDataSet, AccountType> resultMap = Maps.newHashMap(invitables);

                // Remove the ones that already have a raw contact in the current contact
                for (RawContact rawContact : contactData.getRawContacts()) {
                    final AccountTypeWithDataSet type = AccountTypeWithDataSet
                            .get(rawContact.getAccountTypeString(), rawContact.getDataSet());
                    resultMap.remove(type);
                }

                resultListBuilder.addAll(resultMap.values());
            }
        }

        // Set to mInvitableAccountTypes
        contactData.setInvitableAccountTypes(resultListBuilder.build());
    }

    /**
     * Extracts Contact level columns from the cursor.
     */
    private Contact loadContactHeaderData(final Cursor cursor, Uri contactUri) {
        final String directoryParameter = contactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
        final long directoryId = directoryParameter == null ? Directory.DEFAULT
                : Long.parseLong(directoryParameter);
        final long contactId = cursor.getLong(ContactQuery.CONTACT_ID);
        final String lookupKey = cursor.getString(ContactQuery.LOOKUP_KEY);
        final long nameRawContactId = cursor.getLong(ContactQuery.NAME_RAW_CONTACT_ID);
        final int displayNameSource = cursor.getInt(ContactQuery.DISPLAY_NAME_SOURCE);
        final String displayName = cursor.getString(ContactQuery.DISPLAY_NAME);
        final String altDisplayName = cursor.getString(ContactQuery.ALT_DISPLAY_NAME);
        final String phoneticName = cursor.getString(ContactQuery.PHONETIC_NAME);
        final long photoId = cursor.getLong(ContactQuery.PHOTO_ID);
        final String photoUri = cursor.getString(ContactQuery.PHOTO_URI);
        final boolean starred = cursor.getInt(ContactQuery.STARRED) != 0;
        final Integer presence = cursor.isNull(ContactQuery.CONTACT_PRESENCE) ? null
                : cursor.getInt(ContactQuery.CONTACT_PRESENCE);
        final boolean sendToVoicemail = cursor.getInt(ContactQuery.SEND_TO_VOICEMAIL) == 1;
        final String customRingtone = cursor.getString(ContactQuery.CUSTOM_RINGTONE);
        final boolean isUserProfile = cursor.getInt(ContactQuery.IS_USER_PROFILE) == 1;

        Uri lookupUri;
        if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) {
            lookupUri = ContentUris.withAppendedId(Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey),
                    contactId);
        } else {
            lookupUri = contactUri;
        }

        return new Contact(mRequestedUri, contactUri, lookupUri, directoryId, lookupKey, contactId,
                nameRawContactId, displayNameSource, photoId, photoUri, displayName, altDisplayName, phoneticName,
                starred, presence, sendToVoicemail, customRingtone, isUserProfile);
    }

    /**
     * Extracts RawContact level columns from the cursor.
     */
    private ContentValues loadRawContactValues(Cursor cursor) {
        ContentValues cv = new ContentValues();

        cv.put(RawContacts._ID, cursor.getLong(ContactQuery.RAW_CONTACT_ID));

        cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_NAME);
        cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE);
        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SET);
        cursorColumnToContentValues(cursor, cv, ContactQuery.DIRTY);
        cursorColumnToContentValues(cursor, cv, ContactQuery.VERSION);
        cursorColumnToContentValues(cursor, cv, ContactQuery.SOURCE_ID);
        cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC1);
        cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC2);
        cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC3);
        cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC4);
        cursorColumnToContentValues(cursor, cv, ContactQuery.DELETED);
        cursorColumnToContentValues(cursor, cv, ContactQuery.CONTACT_ID);
        cursorColumnToContentValues(cursor, cv, ContactQuery.STARRED);

        return cv;
    }

    /**
     * Extracts Data level columns from the cursor.
     */
    private ContentValues loadDataValues(Cursor cursor) {
        ContentValues cv = new ContentValues();

        cv.put(Data._ID, cursor.getLong(ContactQuery.DATA_ID));

        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA1);
        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA2);
        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA3);
        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA4);
        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA5);
        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA6);
        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA7);
        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA8);
        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA9);
        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA10);
        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA11);
        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA12);
        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA13);
        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA14);
        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA15);
        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC1);
        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC2);
        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC3);
        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC4);
        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_VERSION);
        cursorColumnToContentValues(cursor, cv, ContactQuery.IS_PRIMARY);
        cursorColumnToContentValues(cursor, cv, ContactQuery.IS_SUPERPRIMARY);
        cursorColumnToContentValues(cursor, cv, ContactQuery.MIMETYPE);
        cursorColumnToContentValues(cursor, cv, ContactQuery.GROUP_SOURCE_ID);
        cursorColumnToContentValues(cursor, cv, ContactQuery.CHAT_CAPABILITY);
        cursorColumnToContentValues(cursor, cv, ContactQuery.TIMES_USED);
        cursorColumnToContentValues(cursor, cv, ContactQuery.LAST_TIME_USED);

        return cv;
    }

    private void cursorColumnToContentValues(Cursor cursor, ContentValues values, int index) {
        switch (cursor.getType(index)) {
        case Cursor.FIELD_TYPE_NULL:
            // don't put anything in the content values
            break;
        case Cursor.FIELD_TYPE_INTEGER:
            values.put(ContactQuery.COLUMNS[index], cursor.getLong(index));
            break;
        case Cursor.FIELD_TYPE_STRING:
            values.put(ContactQuery.COLUMNS[index], cursor.getString(index));
            break;
        case Cursor.FIELD_TYPE_BLOB:
            values.put(ContactQuery.COLUMNS[index], cursor.getBlob(index));
            break;
        default:
            throw new IllegalStateException("Invalid or unhandled data type");
        }
    }

    private void loadDirectoryMetaData(Contact result) {
        long directoryId = result.getDirectoryId();

        Cursor cursor = getContext().getContentResolver().query(
                ContentUris.withAppendedId(Directory.CONTENT_URI, directoryId), DirectoryQuery.COLUMNS, null, null,
                null);
        if (cursor == null) {
            return;
        }
        try {
            if (cursor.moveToFirst()) {
                final String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME);
                final String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME);
                final int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID);
                final String accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
                final String accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
                final int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT);
                String directoryType = null;
                if (!TextUtils.isEmpty(packageName)) {
                    PackageManager pm = getContext().getPackageManager();
                    try {
                        Resources resources = pm.getResourcesForApplication(packageName);
                        directoryType = resources.getString(typeResourceId);
                    } catch (NameNotFoundException e) {
                        Log.w(TAG, "Contact directory resource not found: " + packageName + "." + typeResourceId);
                    }
                }

                result.setDirectoryMetaData(displayName, directoryType, accountType, accountName, exportSupport);
            }
        } finally {
            cursor.close();
        }
    }

    static private class AccountKey {
        private final String mAccountName;
        private final String mAccountType;
        private final String mDataSet;

        public AccountKey(String accountName, String accountType, String dataSet) {
            mAccountName = accountName;
            mAccountType = accountType;
            mDataSet = dataSet;
        }

        @Override
        public int hashCode() {
            return Objects.hash(mAccountName, mAccountType, mDataSet);
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof AccountKey)) {
                return false;
            }
            final AccountKey other = (AccountKey) obj;
            return Objects.equals(mAccountName, other.mAccountName)
                    && Objects.equals(mAccountType, other.mAccountType) && Objects.equals(mDataSet, other.mDataSet);
        }
    }

    /**
     * Loads groups meta-data for all groups associated with all constituent raw contacts'
     * accounts.
     */
    private void loadGroupMetaData(Contact result) {
        StringBuilder selection = new StringBuilder();
        ArrayList<String> selectionArgs = new ArrayList<String>();
        final HashSet<AccountKey> accountsSeen = new HashSet<>();
        for (RawContact rawContact : result.getRawContacts()) {
            final String accountName = rawContact.getAccountName();
            final String accountType = rawContact.getAccountTypeString();
            final String dataSet = rawContact.getDataSet();
            final AccountKey accountKey = new AccountKey(accountName, accountType, dataSet);
            if (accountName != null && accountType != null && !accountsSeen.contains(accountKey)) {
                accountsSeen.add(accountKey);
                if (selection.length() != 0) {
                    selection.append(" OR ");
                }
                selection.append("(" + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?");
                selectionArgs.add(accountName);
                selectionArgs.add(accountType);

                if (dataSet != null) {
                    selection.append(" AND " + Groups.DATA_SET + "=?");
                    selectionArgs.add(dataSet);
                } else {
                    selection.append(" AND " + Groups.DATA_SET + " IS NULL");
                }
                selection.append(")");
            }
        }
        final ImmutableList.Builder<GroupMetaData> groupListBuilder = new ImmutableList.Builder<GroupMetaData>();
        final Cursor cursor = getContext().getContentResolver().query(Groups.CONTENT_URI, GroupQuery.COLUMNS,
                selection.toString(), selectionArgs.toArray(new String[0]), null);
        if (cursor != null) {
            try {
                while (cursor.moveToNext()) {
                    final String accountName = cursor.getString(GroupQuery.ACCOUNT_NAME);
                    final String accountType = cursor.getString(GroupQuery.ACCOUNT_TYPE);
                    final String dataSet = cursor.getString(GroupQuery.DATA_SET);
                    final long groupId = cursor.getLong(GroupQuery.ID);
                    final String title = cursor.getString(GroupQuery.TITLE);
                    final boolean defaultGroup = cursor.isNull(GroupQuery.AUTO_ADD) ? false
                            : cursor.getInt(GroupQuery.AUTO_ADD) != 0;
                    final boolean favorites = cursor.isNull(GroupQuery.FAVORITES) ? false
                            : cursor.getInt(GroupQuery.FAVORITES) != 0;

                    groupListBuilder.add(new GroupMetaData(accountName, accountType, dataSet, groupId, title,
                            defaultGroup, favorites));
                }
            } finally {
                cursor.close();
            }
        }
        result.setGroupMetaData(groupListBuilder.build());
    }

    /**
     * Iterates over all data items that represent phone numbers are tries to calculate a formatted
     * number. This function can safely be called several times as no unformatted data is
     * overwritten
     */
    private void computeFormattedPhoneNumbers(Contact contactData) {
        final String countryIso = GeoUtil.getCurrentCountryIso(getContext());
        final ImmutableList<RawContact> rawContacts = contactData.getRawContacts();
        final int rawContactCount = rawContacts.size();
        for (int rawContactIndex = 0; rawContactIndex < rawContactCount; rawContactIndex++) {
            final RawContact rawContact = rawContacts.get(rawContactIndex);
            final List<DataItem> dataItems = rawContact.getDataItems();
            final int dataCount = dataItems.size();
            for (int dataIndex = 0; dataIndex < dataCount; dataIndex++) {
                final DataItem dataItem = dataItems.get(dataIndex);
                if (dataItem instanceof PhoneDataItem) {
                    final PhoneDataItem phoneDataItem = (PhoneDataItem) dataItem;
                    phoneDataItem.computeFormattedPhoneNumber(countryIso);
                }
            }
        }
    }

    @Override
    public void deliverResult(Contact result) {
        unregisterObserver();

        // The creator isn't interested in any further updates
        if (isReset() || result == null) {
            return;
        }

        mContact = result;

        if (result.isLoaded()) {
            mLookupUri = result.getLookupUri();

            if (!result.isDirectoryEntry()) {
                Log.i(TAG, "Registering content observer for " + mLookupUri);
                if (mObserver == null) {
                    mObserver = new ForceLoadContentObserver();
                }
                getContext().getContentResolver().registerContentObserver(mLookupUri, true, mObserver);
            }

            if (mPostViewNotification) {
                // inform the source of the data that this contact is being looked at
                postViewNotificationToSyncAdapter();
            }
        }

        super.deliverResult(mContact);
    }

    /**
     * Posts a message to the contributing sync adapters that have opted-in, notifying them
     * that the contact has just been loaded
     */
    private void postViewNotificationToSyncAdapter() {
        Context context = getContext();
        for (RawContact rawContact : mContact.getRawContacts()) {
            final long rawContactId = rawContact.getId();
            if (mNotifiedRawContactIds.contains(rawContactId)) {
                continue; // Already notified for this raw contact.
            }
            mNotifiedRawContactIds.add(rawContactId);
            final AccountType accountType = rawContact.getAccountType(context);
            final String serviceName = accountType.getViewContactNotifyServiceClassName();
            final String servicePackageName = accountType.getViewContactNotifyServicePackageName();
            if (!TextUtils.isEmpty(serviceName) && !TextUtils.isEmpty(servicePackageName)) {
                final Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
                final Intent intent = new Intent();
                intent.setClassName(servicePackageName, serviceName);
                intent.setAction(Intent.ACTION_VIEW);
                intent.setDataAndType(uri, RawContacts.CONTENT_ITEM_TYPE);
                try {
                    context.startService(intent);
                } catch (Exception e) {
                    Log.e(TAG, "Error sending message to source-app", e);
                }
            }
        }
    }

    private void unregisterObserver() {
        if (mObserver != null) {
            getContext().getContentResolver().unregisterContentObserver(mObserver);
            mObserver = null;
        }
    }

    /**
     * Fully upgrades this ContactLoader to one with all lists fully loaded. When done, the
     * new result will be delivered
     */
    public void upgradeToFullContact() {
        // Everything requested already? Nothing to do, so let's bail out
        if (mLoadGroupMetaData && mLoadInvitableAccountTypes && mPostViewNotification
                && mComputeFormattedPhoneNumber)
            return;

        mLoadGroupMetaData = true;
        mLoadInvitableAccountTypes = true;
        mPostViewNotification = true;
        mComputeFormattedPhoneNumber = true;

        // Cache the current result, so that we only load the "missing" parts of the contact.
        cacheResult();

        // Our load parameters have changed, so let's pretend the data has changed. Its the same
        // thing, essentially.
        onContentChanged();
    }

    public Uri getLookupUri() {
        return mLookupUri;
    }

    @Override
    protected void onStartLoading() {
        if (mContact != null) {
            deliverResult(mContact);
        }

        if (takeContentChanged() || mContact == null) {
            forceLoad();
        }
    }

    @Override
    protected void onStopLoading() {
        cancelLoad();
    }

    @Override
    protected void onReset() {
        super.onReset();
        cancelLoad();
        unregisterObserver();
        mContact = null;
    }

    /**
     * Caches the result, which is useful when we switch from activity to activity, using the same
     * contact. If the next load is for a different contact, the cached result will be dropped
     */
    public void cacheResult() {
        if (mContact == null || !mContact.isLoaded()) {
            sCachedResult = null;
        } else {
            sCachedResult = mContact;
        }
    }
}