Merge "Show avatars in directory search"
diff --git a/res/layout/quickcontact_photo_container.xml b/res/layout/quickcontact_photo_container.xml
index e970934..3b46ef9 100644
--- a/res/layout/quickcontact_photo_container.xml
+++ b/res/layout/quickcontact_photo_container.xml
@@ -26,11 +26,6 @@
             android:layout_height="match_parent"
             android:scaleType="centerCrop" />
         <View
-            android:layout_width="match_parent"
-            android:layout_height="1dip"
-            android:layout_alignParentTop="true"
-            android:background="#4CFFFFFF" />
-        <View
             android:id="@+id/photo_text_bar"
             android:layout_width="0dip"
             android:layout_height="42dip"
diff --git a/src/com/android/contacts/activities/ConfirmAddDetailActivity.java b/src/com/android/contacts/activities/ConfirmAddDetailActivity.java
index 4b297d9..98abfbc 100644
--- a/src/com/android/contacts/activities/ConfirmAddDetailActivity.java
+++ b/src/com/android/contacts/activities/ConfirmAddDetailActivity.java
@@ -319,15 +319,23 @@
         // Apply a limit of 1 result to the query because we only need to
         // determine whether or not at least one other contact has the same
         // name. We don't need to find ALL other contacts with the same name.
-        Builder builder = Contacts.CONTENT_URI.buildUpon();
+        final Builder builder = Contacts.CONTENT_URI.buildUpon();
         builder.appendQueryParameter("limit", String.valueOf(1));
-        Uri uri = builder.build();
+        final Uri uri = builder.build();
 
+        final String displayNameSelection;
+        final String[] selectionArgs;
+        if (TextUtils.isEmpty(contactDisplayName)) {
+            displayNameSelection = Contacts.DISPLAY_NAME_PRIMARY + " IS NULL";
+            selectionArgs = new String[] { String.valueOf(mContactId) };
+        } else {
+            displayNameSelection = Contacts.DISPLAY_NAME_PRIMARY + " = ?";
+            selectionArgs = new String[] { contactDisplayName, String.valueOf(mContactId) };
+        }
         mQueryHandler.startQuery(TOKEN_DISAMBIGUATION_QUERY, null, uri,
                 new String[] { Contacts._ID } /* unused projection but a valid one was needed */,
-                Contacts.DISPLAY_NAME_PRIMARY + " = ? and " + Contacts.PHOTO_ID + " is null and "
-                + Contacts._ID + " <> ?",
-                new String[] { contactDisplayName, String.valueOf(mContactId) }, null);
+                displayNameSelection + " AND " + Contacts.PHOTO_ID + " IS NULL AND "
+                + Contacts._ID + " <> ?", selectionArgs, null);
     }
 
     /**
diff --git a/src/com/android/contacts/editor/LabeledEditorView.java b/src/com/android/contacts/editor/LabeledEditorView.java
index 2a1ec5e..c9e713b 100644
--- a/src/com/android/contacts/editor/LabeledEditorView.java
+++ b/src/com/android/contacts/editor/LabeledEditorView.java
@@ -286,7 +286,17 @@
         }
 
         // Field changes are saved directly
+        saveValue(column, value);
+
+        // Notify listener if applicable
+        notifyEditorListener();
+    }
+
+    protected void saveValue(String column, String value) {
         mEntry.put(column, value);
+    }
+
+    protected void notifyEditorListener() {
         if (mListener != null) {
             mListener.onRequest(EditorListener.FIELD_CHANGED);
         }
diff --git a/src/com/android/contacts/editor/StructuredNameEditorView.java b/src/com/android/contacts/editor/StructuredNameEditorView.java
index 6911628..af1b3cf 100644
--- a/src/com/android/contacts/editor/StructuredNameEditorView.java
+++ b/src/com/android/contacts/editor/StructuredNameEditorView.java
@@ -77,11 +77,12 @@
         if (!isFieldChanged(column, value)) {
             return;
         }
-        super.onFieldChanged(column, value);
 
+        // First save the new value for the column.
+        saveValue(column, value);
         mChanged = true;
 
-        // Make sure the display name and the structured name are synced
+        // Next make sure the display name and the structured name are synced
         if (hasShortAndLongForms()) {
             if (areOptionalFieldsVisible()) {
                 rebuildFullName(getValues());
@@ -89,6 +90,10 @@
                 rebuildStructuredName(getValues());
             }
         }
+
+        // Then notify the listener, which will rely on the display and structured names to be
+        // synced (in order to provide aggregate suggestions).
+        notifyEditorListener();
     }
 
     @Override
diff --git a/src/com/android/contacts/model/AccountType.java b/src/com/android/contacts/model/AccountType.java
index 22ea884..3cc54f1 100644
--- a/src/com/android/contacts/model/AccountType.java
+++ b/src/com/android/contacts/model/AccountType.java
@@ -25,7 +25,6 @@
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.pm.PackageManager;
-import android.database.Cursor;
 import android.graphics.drawable.Drawable;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
@@ -270,6 +269,7 @@
      * {@link Comparator} to sort by {@link DataKind#weight}.
      */
     private static Comparator<DataKind> sWeightComparator = new Comparator<DataKind>() {
+        @Override
         public int compare(DataKind object1, DataKind object2) {
             return object1.weight - object2.weight;
         }
@@ -460,13 +460,12 @@
     }
 
     /**
-     * Generic method of inflating a given {@link Cursor} into a user-readable
+     * Generic method of inflating a given {@link ContentValues} into a user-readable
      * {@link CharSequence}. For example, an inflater could combine the multiple
      * columns of {@link StructuredPostal} together using a string resource
      * before presenting to the user.
      */
     public interface StringInflater {
-        public CharSequence inflateUsing(Context context, Cursor cursor);
         public CharSequence inflateUsing(Context context, ContentValues values);
     }
 
diff --git a/src/com/android/contacts/model/BaseAccountType.java b/src/com/android/contacts/model/BaseAccountType.java
index 4d82ece..ce74148 100644
--- a/src/com/android/contacts/model/BaseAccountType.java
+++ b/src/com/android/contacts/model/BaseAccountType.java
@@ -17,18 +17,13 @@
 package com.android.contacts.model;
 
 import com.android.contacts.R;
-import com.android.contacts.model.AccountType.DefinitionException;
 import com.android.contacts.util.DateUtils;
 import com.google.android.collect.Lists;
 import com.google.android.collect.Maps;
 
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.res.Resources;
-import android.database.Cursor;
 import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.Event;
@@ -48,6 +43,9 @@
 import android.util.Log;
 import android.view.inputmethod.EditorInfo;
 
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
 import java.io.IOException;
 import java.util.List;
 import java.util.Locale;
@@ -466,25 +464,7 @@
             mColumnName = columnName;
         }
 
-        public CharSequence inflateUsing(Context context, Cursor cursor) {
-            final int index = mColumnName != null ? cursor.getColumnIndex(mColumnName) : -1;
-            final boolean validString = mStringRes > 0;
-            final boolean validColumn = index != -1;
-
-            final CharSequence stringValue = validString ? context.getText(mStringRes) : null;
-            final CharSequence columnValue = validColumn ? cursor.getString(index) : null;
-
-            if (validString && validColumn) {
-                return String.format(stringValue.toString(), columnValue);
-            } else if (validString) {
-                return stringValue;
-            } else if (validColumn) {
-                return columnValue;
-            } else {
-                return null;
-            }
-        }
-
+        @Override
         public CharSequence inflateUsing(Context context, ContentValues values) {
             final boolean validColumn = values.containsKey(mColumnName);
             final boolean validString = mStringRes > 0;
@@ -541,12 +521,7 @@
             }
         }
 
-        public CharSequence inflateUsing(Context context, Cursor cursor) {
-            final Integer type = cursor.getInt(cursor.getColumnIndex(getTypeColumn()));
-            final String label = cursor.getString(cursor.getColumnIndex(getLabelColumn()));
-            return getTypeLabel(context.getResources(), type, label);
-        }
-
+        @Override
         public CharSequence inflateUsing(Context context, ContentValues values) {
             final Integer type = values.getAsInteger(getTypeColumn());
             final String label = values.getAsString(getLabelColumn());
diff --git a/src/com/android/contacts/quickcontact/DataAction.java b/src/com/android/contacts/quickcontact/DataAction.java
index 415fa18..7c91ba8 100644
--- a/src/com/android/contacts/quickcontact/DataAction.java
+++ b/src/com/android/contacts/quickcontact/DataAction.java
@@ -21,14 +21,14 @@
 import com.android.contacts.model.AccountType.EditType;
 import com.android.contacts.model.DataKind;
 import com.android.contacts.util.Constants;
-import com.android.contacts.util.StructuredPostalUtils;
 import com.android.contacts.util.PhoneCapabilityTester;
+import com.android.contacts.util.StructuredPostalUtils;
 
 import android.content.ContentUris;
+import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
-import android.database.Cursor;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.net.WebAddress;
@@ -67,7 +67,8 @@
     /**
      * Create an action from common {@link Data} elements.
      */
-    public DataAction(Context context, String mimeType, DataKind kind, long dataId, Cursor cursor) {
+    public DataAction(Context context, String mimeType, DataKind kind, long dataId,
+            ContentValues entryValues) {
         mContext = context;
         mKind = kind;
         mMimeType = mimeType;
@@ -75,9 +76,8 @@
         // Determine type for subtitle
         mSubtitle = "";
         if (kind.typeColumn != null) {
-            final int typeColumnIndex = cursor.getColumnIndex(kind.typeColumn);
-            if (typeColumnIndex != -1) {
-                final int typeValue = cursor.getInt(typeColumnIndex);
+            if (entryValues.containsKey(kind.typeColumn)) {
+                final int typeValue = entryValues.getAsInteger(kind.typeColumn);
 
                 // get type string
                 for (EditType type : kind.typeList) {
@@ -87,8 +87,7 @@
                             mSubtitle = context.getString(type.labelRes);
                         } else {
                             // Custom type. Read it from the database
-                            mSubtitle = cursor.getString(cursor.getColumnIndexOrThrow(
-                                    type.customColumn));
+                            mSubtitle = entryValues.getAsString(type.customColumn);
                         }
                         break;
                     }
@@ -96,12 +95,11 @@
             }
         }
 
-        if (getAsInt(cursor, Data.IS_SUPER_PRIMARY) != 0) {
-            mIsPrimary = true;
-        }
+        final Integer superPrimary = entryValues.getAsInteger(Data.IS_SUPER_PRIMARY);
+        mIsPrimary = superPrimary != null && superPrimary != 0;
 
         if (mKind.actionBody != null) {
-            mBody = mKind.actionBody.inflateUsing(context, cursor);
+            mBody = mKind.actionBody.inflateUsing(context, entryValues);
         }
 
         mDataId = dataId;
@@ -113,7 +111,7 @@
         // Handle well-known MIME-types with special care
         if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
             if (PhoneCapabilityTester.isPhone(mContext)) {
-                final String number = getAsString(cursor, Phone.NUMBER);
+                final String number = entryValues.getAsString(Phone.NUMBER);
                 if (!TextUtils.isEmpty(number)) {
 
                     final Intent phoneIntent = hasPhone ? ContactsUtils.getCallIntent(number)
@@ -136,7 +134,7 @@
             }
         } else if (SipAddress.CONTENT_ITEM_TYPE.equals(mimeType)) {
             if (PhoneCapabilityTester.isSipPhone(mContext)) {
-                final String address = getAsString(cursor, SipAddress.SIP_ADDRESS);
+                final String address = entryValues.getAsString(SipAddress.SIP_ADDRESS);
                 if (!TextUtils.isEmpty(address)) {
                     final Uri callUri = Uri.fromParts(Constants.SCHEME_SIP, address, null);
                     mIntent = ContactsUtils.getCallIntent(callUri);
@@ -149,14 +147,14 @@
                 }
             }
         } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
-            final String address = getAsString(cursor, Email.DATA);
+            final String address = entryValues.getAsString(Email.DATA);
             if (!TextUtils.isEmpty(address)) {
                 final Uri mailUri = Uri.fromParts(Constants.SCHEME_MAILTO, address, null);
                 mIntent = new Intent(Intent.ACTION_SENDTO, mailUri);
             }
 
         } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType)) {
-            final String url = getAsString(cursor, Website.URL);
+            final String url = entryValues.getAsString(Website.URL);
             if (!TextUtils.isEmpty(url)) {
                 WebAddress webAddress = new WebAddress(url);
                 mIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(webAddress.toString()));
@@ -164,10 +162,10 @@
 
         } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType)) {
             final boolean isEmail = Email.CONTENT_ITEM_TYPE.equals(
-                    getAsString(cursor, Data.MIMETYPE));
-            if (isEmail || isProtocolValid(cursor)) {
+                    entryValues.getAsString(Data.MIMETYPE));
+            if (isEmail || isProtocolValid(entryValues)) {
                 final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK :
-                        getAsInt(cursor, Im.PROTOCOL);
+                        entryValues.getAsInteger(Im.PROTOCOL);
 
                 if (isEmail) {
                     // Use Google Talk string when using Email, and clear data
@@ -176,9 +174,8 @@
                     mDataUri = null;
                 }
 
-                String host = getAsString(cursor, Im.CUSTOM_PROTOCOL);
-                String data = getAsString(cursor,
-                        isEmail ? Email.DATA : Im.DATA);
+                String host = entryValues.getAsString(Im.CUSTOM_PROTOCOL);
+                String data = entryValues.getAsString(isEmail ? Email.DATA : Im.DATA);
                 if (protocol != Im.PROTOCOL_CUSTOM) {
                     // Try bringing in a well-known host for specific protocols
                     host = ContactsUtils.lookupProviderNameFromId(protocol);
@@ -192,7 +189,8 @@
 
                     // If the address is also available for a video chat, we'll show the capability
                     // as a secondary action.
-                    final int chatCapability = getAsInt(cursor, Data.CHAT_CAPABILITY);
+                    final Integer chatCapabilityObj = entryValues.getAsInteger(Im.CHAT_CAPABILITY);
+                    final int chatCapability = chatCapabilityObj == null ? 0 : chatCapabilityObj;
                     final boolean isVideoChatCapable =
                             (chatCapability & Im.CAPABILITY_HAS_CAMERA) != 0;
                     final boolean isAudioChatCapable =
@@ -211,7 +209,8 @@
                 }
             }
         } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType)) {
-            final String postalAddress = getAsString(cursor, StructuredPostal.FORMATTED_ADDRESS);
+            final String postalAddress =
+                    entryValues.getAsString(StructuredPostal.FORMATTED_ADDRESS);
             if (!TextUtils.isEmpty(postalAddress)) {
                 mIntent = StructuredPostalUtils.getViewPostalAddressIntent(postalAddress);
             }
@@ -227,25 +226,13 @@
         mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
     }
 
-    /** Read {@link String} from the given {@link Cursor}. */
-    private static String getAsString(Cursor cursor, String columnName) {
-        final int index = cursor.getColumnIndex(columnName);
-        return cursor.getString(index);
-    }
-
-    /** Read {@link Integer} from the given {@link Cursor}. */
-    private static int getAsInt(Cursor cursor, String columnName) {
-        final int index = cursor.getColumnIndex(columnName);
-        return cursor.getInt(index);
-    }
-
-    private boolean isProtocolValid(Cursor cursor) {
-        final int columnIndex = cursor.getColumnIndex(Im.PROTOCOL);
-        if (cursor.isNull(columnIndex)) {
+    private boolean isProtocolValid(ContentValues entryValues) {
+        final String protocol = entryValues.getAsString(Im.PROTOCOL);
+        if (protocol == null) {
             return false;
         }
         try {
-            Integer.valueOf(cursor.getString(columnIndex));
+            Integer.valueOf(protocol);
         } catch (NumberFormatException e) {
             return false;
         }
diff --git a/src/com/android/contacts/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java
index 2c62fe4..7a209b5 100644
--- a/src/com/android/contacts/quickcontact/QuickContactActivity.java
+++ b/src/com/android/contacts/quickcontact/QuickContactActivity.java
@@ -17,44 +17,41 @@
 package com.android.contacts.quickcontact;
 
 import com.android.contacts.Collapser;
+import com.android.contacts.ContactLoader;
 import com.android.contacts.ContactPhotoManager;
 import com.android.contacts.R;
 import com.android.contacts.model.AccountTypeManager;
 import com.android.contacts.model.DataKind;
-import com.android.contacts.util.DataStatus;
-import com.android.contacts.util.NotifyingAsyncQueryHandler;
-import com.android.contacts.util.NotifyingAsyncQueryHandler.AsyncQueryListener;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Lists;
 
 import android.app.Activity;
 import android.app.Fragment;
 import android.app.FragmentManager;
+import android.app.LoaderManager.LoaderCallbacks;
 import android.content.ActivityNotFoundException;
 import android.content.ContentUris;
+import android.content.ContentValues;
 import android.content.Context;
+import android.content.Entity;
+import android.content.Entity.NamedContentValues;
 import android.content.Intent;
+import android.content.Loader;
 import android.content.pm.PackageManager;
-import android.content.res.AssetFileDescriptor;
-import android.database.Cursor;
-import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
-import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.Handler;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.Im;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.Photo;
 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.provider.ContactsContract.CommonDataKinds.Website;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.DisplayPhoto;
 import android.provider.ContactsContract.QuickContact;
 import android.provider.ContactsContract.RawContacts;
 import android.support.v13.app.FragmentPagerAdapter;
@@ -74,7 +71,6 @@
 import android.widget.TextView;
 import android.widget.Toast;
 
-import java.io.IOException;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -96,8 +92,6 @@
     @SuppressWarnings("deprecation")
     private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY;
 
-    private NotifyingAsyncQueryHandler mHandler;
-
     private Uri mLookupUri;
     private String[] mExcludeMimes;
     private List<String> mSortedActionMimeTypes = Lists.newArrayList();
@@ -147,8 +141,8 @@
     private static final List<String> TRAILING_MIMETYPES = Lists.newArrayList(
             StructuredPostal.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE);
 
-    /** Id for the background handler that loads the data */
-    private static final int HANDLER_ID_DATA = 1;
+    /** Id for the background loader */
+    private static final int LOADER_ID = 0;
 
     @Override
     protected void onCreate(Bundle icicle) {
@@ -190,8 +184,6 @@
         mListPager.setAdapter(new ViewPagerAdapter(getFragmentManager()));
         mListPager.setOnPageChangeListener(new PageChangeListener());
 
-        mHandler = new NotifyingAsyncQueryHandler(this, mQueryListener);
-
         show();
     }
 
@@ -225,16 +217,7 @@
         mPhotoContainer = findViewById(R.id.photo_container);
         setHeaderNameText(R.id.name, R.string.missing_name);
 
-        // Start background query for data, but only select photo rows when they
-        // directly match the super-primary PHOTO_ID.
-        final Uri dataUri = Uri.withAppendedPath(lookupUri, Contacts.Data.CONTENT_DIRECTORY);
-        mHandler.cancelOperation(HANDLER_ID_DATA);
-
-        // Select all data items of the contact (except for photos, where we only select the display
-        // photo)
-        mHandler.startQuery(HANDLER_ID_DATA, lookupUri, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
-                + "!=? OR (" + Data.MIMETYPE + "=? AND " + Data._ID + "=" + Contacts.PHOTO_ID
-                + ")", new String[] { Photo.CONTENT_ITEM_TYPE, Photo.CONTENT_ITEM_TYPE }, null);
+        getLoaderManager().initLoader(LOADER_ID, null, mLoaderCallbacks);
     }
 
     private boolean handleOutsideTouch() {
@@ -248,7 +231,7 @@
 
     private void hide(boolean withAnimation) {
         // cancel any pending queries
-        mHandler.cancelOperation(HANDLER_ID_DATA);
+        getLoaderManager().destroyLoader(LOADER_ID);
 
         if (withAnimation) {
             mFloatingLayout.hideChild(new Runnable() {
@@ -268,47 +251,6 @@
         hide(true);
     }
 
-    private final AsyncQueryListener mQueryListener = new AsyncQueryListener() {
-        @Override
-        public synchronized void onQueryComplete(int token, Object cookie, Cursor cursor) {
-            try {
-                if (isFinishing()) {
-                    hide(false);
-                    return;
-                } else if (cursor == null || cursor.getCount() == 0) {
-                    Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage,
-                            Toast.LENGTH_LONG).show();
-                    hide(false);
-                    return;
-                }
-
-                bindData(cursor);
-
-                if (TRACE_LAUNCH) {
-                    android.os.Debug.stopMethodTracing();
-                }
-
-                // Data bound and ready, pull curtain to show. Put this on the Handler to ensure
-                // that the layout passes are completed
-                mHandler.post(new Runnable() {
-                    @Override
-                    public void run() {
-                        mFloatingLayout.showChild(new Runnable() {
-                            @Override
-                            public void run() {
-                                mHasFinishedAnimatingIn = true;
-                            }
-                        });
-                    }
-                });
-            } finally {
-                if (cursor != null) {
-                    cursor.close();
-                }
-            }
-        }
-    };
-
     /** Assign this string to the view if it is not empty. */
     private void setHeaderNameText(int id, int resId) {
         setHeaderNameText(id, getText(resId));
@@ -325,35 +267,6 @@
     }
 
     /**
-     * Assign this string to the view (if found in {@link #mPhotoContainer}), or hiding this view
-     * if there is no string.
-     */
-    private void setHeaderText(int id, int resId) {
-        setHeaderText(id, getText(resId));
-    }
-
-    /**
-     * Assign this string to the view (if found in {@link #mPhotoContainer}), or hiding this view
-     * if there is no string.
-     */
-    private void setHeaderText(int id, CharSequence value) {
-        final View view = mPhotoContainer.findViewById(id);
-        if (view instanceof TextView) {
-            ((TextView)view).setText(value);
-            view.setVisibility(TextUtils.isEmpty(value) ? View.GONE : View.VISIBLE);
-        }
-    }
-
-    /** Assign this image to the view, if found in {@link #mPhotoContainer}. */
-    private void setHeaderImage(int id, Drawable drawable) {
-        final View view = mPhotoContainer.findViewById(id);
-        if (view instanceof ImageView) {
-            ((ImageView)view).setImageDrawable(drawable);
-            view.setVisibility(drawable == null ? View.GONE : View.VISIBLE);
-        }
-    }
-
-    /**
      * Check if the given MIME-type appears in the list of excluded MIME-types
      * that the most-recent caller requested.
      */
@@ -368,9 +281,9 @@
     }
 
     /**
-     * Handle the result from the {@link #TOKEN_DATA} query.
+     * Handle the result from the ContactLoader
      */
-    private void bindData(Cursor cursor) {
+    private void bindData(ContactLoader.Result data) {
         final ResolveCache cache = ResolveCache.getInstance(this);
         final Context context = this;
 
@@ -379,90 +292,62 @@
 
         mDefaultsMap.clear();
 
-        final DataStatus status = new DataStatus();
         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(
                 context.getApplicationContext());
         final ImageView photoView = (ImageView) mPhotoContainer.findViewById(R.id.photo);
+        final byte[] photo = data.getPhotoBinaryData();
+        if (photo != null) {
+            photoView.setImageBitmap(BitmapFactory.decodeByteArray(photo, 0, photo.length));
+        } else {
+            photoView.setImageResource(
+                    ContactPhotoManager.getDefaultAvatarResId(true, false));
+        }
 
-        Bitmap photoBitmap = null;
-        while (cursor.moveToNext()) {
-            // Handle any social status updates from this row
-            status.possibleUpdate(cursor);
+        for (Entity entity : data.getEntities()) {
+            final ContentValues entityValues = entity.getEntityValues();
+            final String accountType = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
+            final String dataSet = entityValues.getAsString(RawContacts.DATA_SET);
+            for (NamedContentValues subValue : entity.getSubValues()) {
+                final ContentValues entryValues = subValue.values;
+                final String mimeType = entryValues.getAsString(Data.MIMETYPE);
 
-            final String mimeType = cursor.getString(DataQuery.MIMETYPE);
+                // Skip this data item if MIME-type excluded
+                if (isMimeExcluded(mimeType)) continue;
 
-            // Skip this data item if MIME-type excluded
-            if (isMimeExcluded(mimeType)) continue;
+                final long dataId = entryValues.getAsLong(Data._ID);
+                final Integer primary = entryValues.getAsInteger(Data.IS_PRIMARY);
+                final boolean isPrimary = primary != null && primary != 0;
+                final Integer superPrimary = entryValues.getAsInteger(Data.IS_SUPER_PRIMARY);
+                final boolean isSuperPrimary = superPrimary != null && superPrimary != 0;
 
-            final long dataId = cursor.getLong(DataQuery._ID);
-            final String accountType = cursor.getString(DataQuery.ACCOUNT_TYPE);
-            final String dataSet = cursor.getString(DataQuery.DATA_SET);
-            final boolean isPrimary = cursor.getInt(DataQuery.IS_PRIMARY) != 0;
-            final boolean isSuperPrimary = cursor.getInt(DataQuery.IS_SUPER_PRIMARY) != 0;
+                final DataKind kind =
+                        accountTypes.getKindOrFallback(accountType, dataSet, mimeType);
 
-            // Handle photos included as data row
-            if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
-                final int displayPhotoColumnIndex = cursor.getColumnIndex(Photo.PHOTO_FILE_ID);
-                final boolean hasDisplayPhoto = !cursor.isNull(displayPhotoColumnIndex);
-                if (hasDisplayPhoto) {
-                    final long displayPhotoId = cursor.getLong(displayPhotoColumnIndex);
-                    final Uri displayPhotoUri = ContentUris.withAppendedId(
-                            DisplayPhoto.CONTENT_URI, displayPhotoId);
-                    // Fetch and JPEG uncompress on the background thread
-                    new AsyncTask<Void, Void, Bitmap>() {
-                        @Override
-                        protected Bitmap doInBackground(Void... params) {
-                            try {
-                                AssetFileDescriptor fd = getContentResolver()
-                                        .openAssetFileDescriptor(displayPhotoUri, "r");
-                                return BitmapFactory.decodeStream(fd.createInputStream());
-                            } catch (IOException e) {
-                                Log.e(TAG, "Error getting display photo. Ignoring, as we already " +
-                                        "have the thumbnail", e);
-                                return null;
-                            }
+                if (kind != null) {
+                    // Build an action for this data entry, find a mapping to a UI
+                    // element, build its summary from the cursor, and collect it
+                    // along with all others of this MIME-type.
+                    final Action action = new DataAction(context, mimeType, kind, dataId,
+                            entryValues);
+                    final boolean wasAdded = considerAdd(action, cache);
+                    if (wasAdded) {
+                        // Remember the default
+                        if (isSuperPrimary || (isPrimary && (mDefaultsMap.get(mimeType) == null))) {
+                            mDefaultsMap.put(mimeType, action);
                         }
-
-                        @Override
-                        protected void onPostExecute(Bitmap result) {
-                            if (result == null) return;
-                            photoView.setImageBitmap(result);
-                        }
-                    }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
-                }
-                final int photoColumnIndex = cursor.getColumnIndex(Photo.PHOTO);
-                final byte[] photoBlob = cursor.getBlob(photoColumnIndex);
-                if (photoBlob != null) {
-                    photoBitmap = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
-                }
-                continue;
-            }
-
-            final DataKind kind = accountTypes.getKindOrFallback(accountType, dataSet, mimeType);
-
-            if (kind != null) {
-                // Build an action for this data entry, find a mapping to a UI
-                // element, build its summary from the cursor, and collect it
-                // along with all others of this MIME-type.
-                final Action action = new DataAction(context, mimeType, kind, dataId, cursor);
-                final boolean wasAdded = considerAdd(action, cache);
-                if (wasAdded) {
-                    // Remember the default
-                    if (isSuperPrimary || (isPrimary && (mDefaultsMap.get(mimeType) == null))) {
-                        mDefaultsMap.put(mimeType, action);
                     }
                 }
-            }
 
-            // Handle Email rows with presence data as Im entry
-            final boolean hasPresence = !cursor.isNull(DataQuery.PRESENCE);
-            if (hasPresence && Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
-                final DataKind imKind = accountTypes.getKindOrFallback(accountType, dataSet,
-                        Im.CONTENT_ITEM_TYPE);
-                if (imKind != null) {
-                    final DataAction action = new DataAction(context, Im.CONTENT_ITEM_TYPE, imKind,
-                            dataId, cursor);
-                    considerAdd(action, cache);
+                // Handle Email rows with presence data as Im entry
+                final boolean hasPresence = data.getStatuses().containsKey(dataId);
+                if (hasPresence && Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                    final DataKind imKind = accountTypes.getKindOrFallback(accountType, dataSet,
+                            Im.CONTENT_ITEM_TYPE);
+                    if (imKind != null) {
+                        final DataAction action = new DataAction(context, Im.CONTENT_ITEM_TYPE,
+                                imKind, dataId, entryValues);
+                        considerAdd(action, cache);
+                    }
                 }
             }
         }
@@ -472,20 +357,7 @@
             Collapser.collapseList(actionChildren);
         }
 
-        if (cursor.moveToLast()) {
-            // Read contact name from last data row
-            final String name = cursor.getString(DataQuery.DISPLAY_NAME);
-            setHeaderNameText(R.id.name, name);
-        }
-
-        if (photoView != null) {
-            // Place photo when discovered in data, otherwise show generic avatar
-            if (photoBitmap != null) {
-                photoView.setImageBitmap(photoBitmap);
-            } else {
-                photoView.setImageResource(ContactPhotoManager.getDefaultAvatarResId(true, false));
-            }
-        }
+        setHeaderNameText(R.id.name, data.getDisplayName());
 
         // All the mime-types to add.
         final Set<String> containedTypes = new HashSet<String>(mActions.keySet());
@@ -515,6 +387,7 @@
         }
 
         // Add buttons for each mimetype
+        mTrack.removeAllViews();
         for (String mimeType : mSortedActionMimeTypes) {
             final View actionView = inflateAction(mimeType, cache, mTrack);
             mTrack.addView(actionView);
@@ -574,6 +447,61 @@
         listFragment.setListener(mListFragmentListener);
     }
 
+    private LoaderCallbacks<ContactLoader.Result> mLoaderCallbacks =
+            new LoaderCallbacks<ContactLoader.Result>() {
+        @Override
+        public void onLoaderReset(Loader<ContactLoader.Result> loader) {
+        }
+
+        @Override
+        public void onLoadFinished(Loader<ContactLoader.Result> loader, ContactLoader.Result data) {
+            if (isFinishing()) {
+                hide(false);
+                return;
+            }
+            if (data.isError()) {
+                // This shouldn't ever happen, so throw an exception. The {@link ContactLoader}
+                // should log the actual exception.
+                throw new IllegalStateException("Failed to load contact", data.getException());
+            }
+            if (data.isNotFound()) {
+                Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri());
+                Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage,
+                        Toast.LENGTH_LONG).show();
+                hide(false);
+                return;
+            }
+
+            bindData(data);
+
+            if (TRACE_LAUNCH) {
+                android.os.Debug.stopMethodTracing();
+            }
+
+            // Data bound and ready, pull curtain to show. Put this on the Handler to ensure
+            // that the layout passes are completed
+            new Handler().post(new Runnable() {
+                @Override
+                public void run() {
+                    mFloatingLayout.showChild(new Runnable() {
+                        @Override
+                        public void run() {
+                            mHasFinishedAnimatingIn = true;
+                        }
+                    });
+                }
+            });
+        }
+
+        @Override
+        public Loader<ContactLoader.Result> onCreateLoader(int id, Bundle args) {
+            if (mLookupUri == null) {
+                Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early");
+            }
+            return new ContactLoader(getApplicationContext(), mLookupUri);
+        }
+    };
+
     /** A type (e.g. Call/Addresses was clicked) */
     private final OnClickListener mTypeViewClickListener = new OnClickListener() {
         @Override
@@ -653,53 +581,4 @@
             new Handler().post(startAppRunnable);
         }
     };
-
-    private interface DataQuery {
-        final String[] PROJECTION = new String[] {
-                Data._ID,
-
-                RawContacts.ACCOUNT_TYPE,
-                RawContacts.DATA_SET,
-                Contacts.STARRED,
-                Contacts.DISPLAY_NAME,
-
-                Data.STATUS,
-                Data.STATUS_RES_PACKAGE,
-                Data.STATUS_ICON,
-                Data.STATUS_LABEL,
-                Data.STATUS_TIMESTAMP,
-                Data.PRESENCE,
-                Data.CHAT_CAPABILITY,
-
-                Data.RES_PACKAGE,
-                Data.MIMETYPE,
-                Data.IS_PRIMARY,
-                Data.IS_SUPER_PRIMARY,
-                Data.RAW_CONTACT_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,
-        };
-
-        final int _ID = 0;
-
-        final int ACCOUNT_TYPE = 1;
-        final int DATA_SET = 2;
-        final int STARRED = 3;
-        final int DISPLAY_NAME = 4;
-
-        final int STATUS = 5;
-        final int STATUS_RES_PACKAGE = 6;
-        final int STATUS_ICON = 7;
-        final int STATUS_LABEL = 8;
-        final int STATUS_TIMESTAMP = 9;
-        final int PRESENCE = 10;
-        final int CHAT_CAPABILITY = 11;
-
-        final int RES_PACKAGE = 12;
-        final int MIMETYPE = 13;
-        final int IS_PRIMARY = 14;
-        final int IS_SUPER_PRIMARY = 15;
-    }
 }
diff --git a/tests/res/values/donottranslate_strings.xml b/tests/res/values/donottranslate_strings.xml
index 19ebde3..c3cbc10 100644
--- a/tests/res/values/donottranslate_strings.xml
+++ b/tests/res/values/donottranslate_strings.xml
@@ -23,7 +23,7 @@
         <!-- List modes -->
         <item>LIST_DEFAULT</item>
         <item>LIST_ALL_CONTACTS_ACTION</item>
-        <item>LIST_CONTACTS_WITH_PHONES_ACTION</item>
+        <item>LIST_CONTACTS_WITH_PHONES_ACTION (deprecated)</item>
         <item>LIST_STARRED_ACTION</item>
         <item>LIST_FREQUENT_ACTION</item>
         <item>LIST_STREQUENT_ACTION</item>