About Card

Change-Id: Idfe396d0a4fa24214599990c2895ed9569e5c0fa
diff --git a/src/com/android/contacts/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java
index b7b301d..7617128 100644
--- a/src/com/android/contacts/quickcontact/QuickContactActivity.java
+++ b/src/com/android/contacts/quickcontact/QuickContactActivity.java
@@ -24,38 +24,54 @@
 import android.app.Activity;
 import android.app.Fragment;
 import android.app.LoaderManager.LoaderCallbacks;
+import android.app.SearchManager;
 import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
 import android.content.ContentUris;
 import android.content.Intent;
 import android.content.Loader;
 import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.graphics.Bitmap;
 import android.graphics.Color;
-import android.graphics.ColorFilter;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffColorFilter;
+import android.net.ParseException;
 import android.net.Uri;
+import android.net.WebAddress;
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.Trace;
+import android.provider.CalendarContract;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Identity;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.provider.ContactsContract.CommonDataKinds.Website;
 import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.DisplayNameSources;
 import android.provider.ContactsContract.QuickContact;
 import android.provider.ContactsContract.RawContacts;
 import android.support.v7.graphics.Palette;
 import android.text.TextUtils;
 import android.util.Log;
+import android.util.Pair;
 import android.view.Menu;
-import android.view.MenuItem;
 import android.view.MenuInflater;
+import android.view.MenuItem;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.WindowManager;
@@ -66,6 +82,9 @@
 import com.android.contacts.ContactSaveService;
 import com.android.contacts.ContactsActivity;
 import com.android.contacts.R;
+import com.android.contacts.common.CallUtil;
+import com.android.contacts.common.Collapser;
+import com.android.contacts.common.ContactsUtils;
 import com.android.contacts.common.editor.SelectAccountDialogFragment;
 import com.android.contacts.common.lettertiles.LetterTileDrawable;
 import com.android.contacts.common.list.ShortcutIntentBuilder;
@@ -79,9 +98,18 @@
 import com.android.contacts.common.model.dataitem.DataItem;
 import com.android.contacts.common.model.dataitem.DataKind;
 import com.android.contacts.common.model.dataitem.EmailDataItem;
+import com.android.contacts.common.model.dataitem.EventDataItem;
 import com.android.contacts.common.model.dataitem.ImDataItem;
+import com.android.contacts.common.model.dataitem.NicknameDataItem;
+import com.android.contacts.common.model.dataitem.NoteDataItem;
+import com.android.contacts.common.model.dataitem.OrganizationDataItem;
 import com.android.contacts.common.model.dataitem.PhoneDataItem;
-import com.android.contacts.common.util.DataStatus;
+import com.android.contacts.common.model.dataitem.RelationDataItem;
+import com.android.contacts.common.model.dataitem.SipAddressDataItem;
+import com.android.contacts.common.model.dataitem.StructuredNameDataItem;
+import com.android.contacts.common.model.dataitem.StructuredPostalDataItem;
+import com.android.contacts.common.model.dataitem.WebsiteDataItem;
+import com.android.contacts.common.util.DateUtils;
 import com.android.contacts.detail.ContactDetailDisplayUtils;
 import com.android.contacts.interactions.CalendarInteractionsLoader;
 import com.android.contacts.interactions.CallLogInteractionsLoader;
@@ -91,16 +119,21 @@
 import com.android.contacts.quickcontact.ExpandingEntryCardView.Entry;
 import com.android.contacts.quickcontact.ExpandingEntryCardView.ExpandingEntryCardViewListener;
 import com.android.contacts.util.ImageViewDrawableSetter;
+import com.android.contacts.util.PhoneCapabilityTester;
 import com.android.contacts.util.SchedulingUtils;
+import com.android.contacts.util.StructuredPostalUtils;
 import com.android.contacts.widget.MultiShrinkScroller;
 import com.android.contacts.widget.MultiShrinkScroller.MultiShrinkScrollerListener;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Lists;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Calendar;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -146,17 +179,31 @@
 
     private ImageView mPhotoView;
     private View mTransparentView;
-    private ExpandingEntryCardView mCommunicationCard;
+    private ExpandingEntryCardView mContactCard;
     private ExpandingEntryCardView mRecentCard;
+    private ExpandingEntryCardView mAboutCard;
+    /**
+     * This list contains all the {@link DataItem}s. Each nested list contains all data items of a
+     * specific mimetype in sorted order, using mWithinMimeTypeDataItemComparator. The mimetype
+     * lists are sorted using mAmongstMimeTypeDataItemComparator.
+     */
+    private List<List<DataItem>> mDataItemsList;
+    /**
+     * A map between a mimetype string and the corresponding list of data items. The data items
+     * are in sorted order using mWithinMimeTypeDataItemComparator.
+     */
+    private Map<String, List<DataItem>> mDataItemsMap;
     private MultiShrinkScroller mScroller;
     private SelectAccountDialogFragmentListener mSelectAccountFragmentListener;
-    private AsyncTask<Void, Void, Void> mEntriesAndActionsTask;
+    private AsyncTask<Void, Void, Pair<List<List<DataItem>>, Map<String, List<DataItem>>>>
+            mEntriesAndActionsTask;
     private ColorDrawable mWindowScrim;
     private boolean mIsWaitingForOtherPieceOfExitAnimation;
     private boolean mIsExitAnimationInProgress;
     private boolean mHasComputedThemeColor;
+    private ComponentName mSmsComponent;
 
-    private static final int MIN_NUM_COMMUNICATION_ENTRIES_SHOWN = 3;
+    private static final int MIN_NUM_CONTACT_ENTRIES_SHOWN = 3;
     private static final int MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN = 3;
 
     private Contact mContactData;
@@ -166,11 +213,6 @@
     private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter();
 
     /**
-     * Keeps the default action per mimetype. Empty if no default actions are set
-     */
-    private HashMap<String, Action> mDefaultsMap = new HashMap<String, Action>();
-
-    /**
      * {@link #LEADING_MIMETYPES} and {@link #TRAILING_MIMETYPES} are used to sort MIME-types.
      *
      * <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog,
@@ -188,6 +230,11 @@
     private static final List<String> TRAILING_MIMETYPES = Lists.newArrayList(
             StructuredPostal.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE);
 
+    private static final List<String> ABOUT_CARD_MIMETYPES = Lists.newArrayList(
+            Event.CONTENT_ITEM_TYPE, GroupMembership.CONTENT_ITEM_TYPE, Identity.CONTENT_ITEM_TYPE,
+            Im.CONTENT_ITEM_TYPE, Nickname.CONTENT_ITEM_TYPE, Note.CONTENT_ITEM_TYPE,
+            Organization.CONTENT_ITEM_TYPE, Relation.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE);
+
     /** Id for the background contact loader */
     private static final int LOADER_CONTACT_ID = 0;
 
@@ -226,7 +273,7 @@
         @Override
         public void onClick(View v) {
             Log.i(TAG, "mEntryClickHandler onClick");
-            Object intent = v.getTag();
+            final Object intent = v.getTag();
             if (intent == null || !(intent instanceof Intent)) {
                 return;
             }
@@ -316,6 +363,88 @@
         }
     };
 
+
+    /**
+     * Data items are compared to the same mimetype based off of three qualities:
+     * 1. Super primary
+     * 2. Primary
+     * 3. Times used
+     */
+    private final Comparator<DataItem> mWithinMimeTypeDataItemComparator =
+            new Comparator<DataItem>() {
+        @Override
+        public int compare(DataItem lhs, DataItem rhs) {
+            if (!lhs.getMimeType().equals(rhs.getMimeType())) {
+                Log.wtf(TAG, "Comparing DataItems with different mimetypes lhs.getMimeType(): " +
+                        lhs.getMimeType() + " rhs.getMimeType(): " + rhs.getMimeType());
+                return 0;
+            }
+
+            if (lhs.isSuperPrimary()) {
+                return -1;
+            } else if (rhs.isSuperPrimary()) {
+                return 1;
+            } else if (lhs.isPrimary() && !rhs.isPrimary()) {
+                return -1;
+            } else if (!lhs.isPrimary() && rhs.isPrimary()) {
+                return 1;
+            } else {
+                final int lhsTimesUsed =
+                        lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed();
+                final int rhsTimesUsed =
+                        rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed();
+
+                return rhsTimesUsed - lhsTimesUsed;
+            }
+        }
+    };
+
+    private final Comparator<List<DataItem>> mAmongstMimeTypeDataItemComparator =
+            new Comparator<List<DataItem>> () {
+        @Override
+        public int compare(List<DataItem> lhsList, List<DataItem> rhsList) {
+            DataItem lhs = lhsList.get(0);
+            DataItem rhs = rhsList.get(0);
+            final int lhsTimesUsed = lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed();
+            final int rhsTimesUsed = rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed();
+            final int timesUsedDifference = rhsTimesUsed - lhsTimesUsed;
+            if (timesUsedDifference != 0) {
+                return timesUsedDifference;
+            }
+
+            final long lhsLastTimeUsed =
+                    lhs.getLastTimeUsed() == null ? 0 : lhs.getLastTimeUsed();
+            final long rhsLastTimeUsed =
+                    rhs.getLastTimeUsed() == null ? 0 : rhs.getLastTimeUsed();
+            final long lastTimeUsedDifference = rhsLastTimeUsed - lhsLastTimeUsed;
+            if (lastTimeUsedDifference > 0) {
+                return 1;
+            } else if (lastTimeUsedDifference < 0) {
+                return -1;
+            }
+
+            // Times used and last time used are the same. Resort to statically defined.
+            final String lhsMimeType = lhs.getMimeType();
+            final String rhsMimeType = rhs.getMimeType();
+            for (String mimeType : LEADING_MIMETYPES) {
+                if (lhsMimeType.equals(mimeType)) {
+                    return -1;
+                } else if (rhsMimeType.equals(mimeType)) {
+                    return 1;
+                }
+            }
+            // Trailing types come last, so flip the returns
+            for (String mimeType : TRAILING_MIMETYPES) {
+                if (lhsMimeType.equals(mimeType)) {
+                    return 1;
+                } else if (rhsMimeType.equals(mimeType)) {
+                    return -1;
+                }
+            }
+            return 0;
+        }
+    };
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         Trace.beginSection("onCreate()");
@@ -334,18 +463,23 @@
 
         setContentView(R.layout.quickcontact_activity);
 
-        mCommunicationCard = (ExpandingEntryCardView) findViewById(R.id.communication_card);
+        mSmsComponent = PhoneCapabilityTester.getSmsComponent(this);
+
+        mContactCard = (ExpandingEntryCardView) findViewById(R.id.communication_card);
         mRecentCard = (ExpandingEntryCardView) findViewById(R.id.recent_card);
+        mAboutCard = (ExpandingEntryCardView) findViewById(R.id.about_card);
         mScroller = (MultiShrinkScroller) findViewById(R.id.multiscroller);
 
-        mCommunicationCard.setOnClickListener(mEntryClickHandler);
-        mCommunicationCard.setTitle(getResources().getString(R.string.communication_card_title));
-        mCommunicationCard.setExpandButtonText(
+        mContactCard.setOnClickListener(mEntryClickHandler);
+        mContactCard.setTitle(getResources().getString(R.string.communication_card_title));
+        mContactCard.setExpandButtonText(
         getResources().getString(R.string.expanding_entry_card_view_see_all));
 
         mRecentCard.setOnClickListener(mEntryClickHandler);
         mRecentCard.setTitle(getResources().getString(R.string.recent_card_title));
 
+        mAboutCard.setOnClickListener(mEntryClickHandler);
+
         mPhotoView = (ImageView) findViewById(R.id.photo);
         mTransparentView = findViewById(R.id.transparent_view);
         if (mScroller != null) {
@@ -520,8 +654,6 @@
         mContactData = data;
         invalidateOptionsMenu();
 
-        mDefaultsMap.clear();
-
         Trace.endSection();
         Trace.beginSection("Set display photo & name");
 
@@ -532,28 +664,26 @@
 
         Trace.endSection();
 
-        // Maintain a list of phone numbers to pass into SmsInteractionsLoader
-        final Set<String> phoneNumbers = new HashSet<>();
-        // Maintain a list of email addresses to pass into CalendarInteractionsLoader
-        final Set<String> emailAddresses = new HashSet<>();
-        // List of Entry that makes up the ExpandingEntryCardView
-        final List<Entry> entries = Lists.newArrayList();
+        mEntriesAndActionsTask = new AsyncTask<Void, Void,
+                Pair<List<List<DataItem>>, Map<String, List<DataItem>>>>() {
 
-        mEntriesAndActionsTask = new AsyncTask<Void, Void, Void>() {
             @Override
-            protected Void doInBackground(Void... params) {
-                computeEntriesAndActions(data, phoneNumbers, emailAddresses, entries);
-                return null;
+            protected Pair<List<List<DataItem>>, Map<String, List<DataItem>>> doInBackground(
+                    Void... params) {
+                return generateDataModelFromContact(data);
             }
 
             @Override
-            protected void onPostExecute(Void aVoid) {
-                super.onPostExecute(aVoid);
+            protected void onPostExecute(Pair<List<List<DataItem>>,
+                    Map<String, List<DataItem>>> dataItemsPair) {
+                super.onPostExecute(dataItemsPair);
+                mDataItemsList = dataItemsPair.first;
+                mDataItemsMap = dataItemsPair.second;
                 // Check that original AsyncTask parameters are still valid and the activity
                 // is still running before binding to UI. A new intent could invalidate
                 // the results, for example.
                 if (data == mContactData && !isCancelled()) {
-                    bindEntriesAndActions(entries, phoneNumbers, emailAddresses);
+                    bindDataToCards();
                     showActivity();
                 }
             }
@@ -561,13 +691,24 @@
         mEntriesAndActionsTask.execute();
     }
 
-    private void bindEntriesAndActions(List<Entry> entries,
-            Set<String> phoneNumbers,
-            Set<String> emailAddresses) {
-        Trace.beginSection("start sms loader");
+    private void bindDataToCards() {
+        startInteractionLoaders();
+        populateContactAndAboutCard();
+    }
+
+    private void startInteractionLoaders() {
+        final List<DataItem> phoneDataItems = mDataItemsMap.get(Phone.CONTENT_ITEM_TYPE);
+        String[] phoneNumbers = null;
+        if (phoneDataItems != null) {
+            phoneNumbers = new String[phoneDataItems.size()];
+            for (int i = 0; i < phoneDataItems.size(); ++i) {
+                phoneNumbers[i] = ((PhoneDataItem) phoneDataItems.get(i)).getNumber();
+            }
+        }
         final Bundle phonesExtraBundle = new Bundle();
-        phonesExtraBundle.putStringArray(KEY_LOADER_EXTRA_PHONES,
-                phoneNumbers.toArray(new String[phoneNumbers.size()]));
+        phonesExtraBundle.putStringArray(KEY_LOADER_EXTRA_PHONES, phoneNumbers);
+
+        Trace.beginSection("start sms loader");
         getLoaderManager().initLoader(
                 LOADER_SMS_ID,
                 phonesExtraBundle,
@@ -581,27 +722,23 @@
                 mLoaderInteractionsCallbacks);
         Trace.endSection();
 
+
         Trace.beginSection("start calendar loader");
+        final List<DataItem> emailDataItems = mDataItemsMap.get(Email.CONTENT_ITEM_TYPE);
+        String[] emailAddresses = null;
+        if (emailDataItems != null) {
+            emailAddresses = new String[emailDataItems.size()];
+            for (int i = 0; i < emailDataItems.size(); ++i) {
+                emailAddresses[i] = ((EmailDataItem) emailDataItems.get(i)).getAddress();
+            }
+        }
         final Bundle emailsExtraBundle = new Bundle();
-        emailsExtraBundle.putStringArray(KEY_LOADER_EXTRA_EMAILS,
-                emailAddresses.toArray(new String[emailAddresses.size()]));
+        emailsExtraBundle.putStringArray(KEY_LOADER_EXTRA_EMAILS, emailAddresses);
         getLoaderManager().initLoader(
                 LOADER_CALENDAR_ID,
                 emailsExtraBundle,
                 mLoaderInteractionsCallbacks);
         Trace.endSection();
-
-        Trace.beginSection("bind communicate card");
-        if (entries.size() > 0) {
-            mCommunicationCard.initialize(entries,
-                    /* numInitialVisibleEntries = */ MIN_NUM_COMMUNICATION_ENTRIES_SHOWN,
-                    /* isExpanded = */ false, mExpandingEntryCardViewListener);
-        }
-
-        final boolean hasData = !entries.isEmpty();
-        mCommunicationCard.setVisibility(hasData ? View.VISIBLE : View.GONE);
-
-        Trace.endSection();
     }
 
     private void showActivity() {
@@ -617,157 +754,322 @@
         }
     }
 
-    private void computeEntriesAndActions(Contact data, Set<String> phoneNumbers,
-            Set<String> emailAddresses, List<Entry> entries) {
-        Trace.beginSection("inflate entries and actions");
+    private void populateContactAndAboutCard() {
+        Trace.beginSection("bind contact card");
 
-        // Map from {@link String} MIME-type to a list of {@link Action}.
-        final ActionMultiMap actions = new ActionMultiMap();
+        final List<Entry> contactCardEntries = new ArrayList<>();
+        final List<Entry> aboutCardEntries = new ArrayList<>();
+
+        int topContactIndex = 0;
+        for (int i = 0; i < mDataItemsList.size(); ++i) {
+            final List<DataItem> dataItemsByMimeType = mDataItemsList.get(i);
+            final DataItem topDataItem = dataItemsByMimeType.get(0);
+            if (ABOUT_CARD_MIMETYPES.contains(topDataItem.getMimeType())) {
+                aboutCardEntries.addAll(dataItemsToEntries(mDataItemsList.get(i)));
+            } else {
+                // Add most used to the top of the contact card
+                final Entry topEntry = dataItemToEntry(topDataItem);
+                if (topEntry != null) {
+                    contactCardEntries.add(topContactIndex++, dataItemToEntry(topDataItem));
+                }
+                // TODO merge SMS into secondary action
+                if (topDataItem instanceof PhoneDataItem) {
+                    final PhoneDataItem phone = (PhoneDataItem) topDataItem;
+                    Intent smsIntent = null;
+                    if (mSmsComponent != null) {
+                        smsIntent = new Intent(Intent.ACTION_SENDTO,
+                                Uri.fromParts(CallUtil.SCHEME_SMSTO, phone.getNumber(), null));
+                        smsIntent.setComponent(mSmsComponent);
+                    }
+                    contactCardEntries.add(topContactIndex++,
+                            new Entry(
+                                    getResources().getDrawable(R.drawable.ic_message_24dp),
+                                    getResources().getString(R.string.send_message),
+                                    /* subHeader = */ null,
+                                    /* text = */ phone.buildDataString(
+                                            this, topDataItem.getDataKind()),
+                                            smsIntent,
+                                            /* isEditable = */ false));
+                }
+                // Add the rest of the entries to the bottom of the card
+                if (dataItemsByMimeType.size() > 1) {
+                    contactCardEntries.addAll(dataItemsToEntries(
+                            dataItemsByMimeType.subList(1, dataItemsByMimeType.size())));
+                }
+            }
+        }
+
+        if (contactCardEntries.size() > 0) {
+            mContactCard.initialize(contactCardEntries,
+                    /* numInitialVisibleEntries = */ MIN_NUM_CONTACT_ENTRIES_SHOWN,
+                    /* isExpanded = */ false,
+                    mExpandingEntryCardViewListener);
+            mContactCard.setVisibility(View.VISIBLE);
+        } else {
+            mContactCard.setVisibility(View.GONE);
+        }
+        Trace.endSection();
+
+        Trace.beginSection("bind about card");
+        mAboutCard.initialize(aboutCardEntries,
+                /* numInitialVisibleEntries = */ 1,
+                /* isExpanded = */ true,
+                mExpandingEntryCardViewListener);
+        Trace.endSection();
+    }
+
+    /**
+     * Builds the {@link DataItem}s Map out of the Contact.
+     * @param data The contact to build the data from.
+     * @return A pair containing a list of data items sorted within mimetype and sorted
+     *  amongst mimetype. The map goes from mimetype string to the sorted list of data items within
+     *  mimetype
+     */
+    private Pair<List<List<DataItem>>, Map<String, List<DataItem>>> generateDataModelFromContact(
+            Contact data) {
+        Trace.beginSection("Build data items map");
+
+        final Map<String, List<DataItem>> dataItemsMap = new HashMap<>();
 
         final ResolveCache cache = ResolveCache.getInstance(this);
         for (RawContact rawContact : data.getRawContacts()) {
             for (DataItem dataItem : rawContact.getDataItems()) {
+                dataItem.setRawContactId(rawContact.getId());
+
                 final String mimeType = dataItem.getMimeType();
+                if (mimeType == null) continue;
+
                 final AccountType accountType = rawContact.getAccountType(this);
                 final DataKind dataKind = AccountTypeManager.getInstance(this)
                         .getKindOrFallback(accountType, mimeType);
+                if (dataKind == null) continue;
 
-                if (dataItem instanceof PhoneDataItem) {
-                    phoneNumbers.add(((PhoneDataItem) dataItem).getNormalizedNumber());
+                dataItem.setDataKind(dataKind);
+
+                final boolean hasData = !TextUtils.isEmpty(dataItem.buildDataString(this,
+                        dataKind));
+
+                if (isMimeExcluded(mimeType) || !hasData) continue;
+
+                List<DataItem> dataItemListByType = dataItemsMap.get(mimeType);
+                if (dataItemListByType == null) {
+                    dataItemListByType = new ArrayList<>();
+                    dataItemsMap.put(mimeType, dataItemListByType);
                 }
-
-                if (dataItem instanceof EmailDataItem) {
-                    emailAddresses.add(((EmailDataItem) dataItem).getAddress());
-                }
-
-                // Skip this data item if MIME-type excluded
-                if (isMimeExcluded(mimeType)) continue;
-
-                final long dataId = dataItem.getId();
-                final boolean isPrimary = dataItem.isPrimary();
-                final boolean isSuperPrimary = dataItem.isSuperPrimary();
-
-                if (dataKind != 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(getApplicationContext(),
-                            dataItem, dataKind);
-                    final boolean wasAdded = considerAdd(action, cache, isSuperPrimary, actions);
-                    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 DataStatus status = data.getStatuses().get(dataId);
-                if (status != null && dataItem instanceof EmailDataItem) {
-                    final EmailDataItem email = (EmailDataItem) dataItem;
-                    final ImDataItem im = ImDataItem.createFromEmail(email);
-                    if (dataKind != null) {
-                        final DataAction action = new DataAction(getApplicationContext(),
-                                im, dataKind);
-                        action.setPresence(status.getPresence());
-                        considerAdd(action, cache, isSuperPrimary, actions);
-                    }
-                }
+                dataItemListByType.add(dataItem);
             }
         }
-
         Trace.endSection();
-        Trace.beginSection("collapsing action list");
 
-        Trace.endSection();
-        Trace.beginSection("sort mimetypes");
-
+        Trace.beginSection("sort within mimetypes");
         /*
          * Sorting is a multi part step. The end result is to a have a sorted list of the most
-         * used actions, one per mimetype. Then, within each mimetype, the list of actions for that
-         * type is also sorted, based off of {super primary, primary, times used} in that order.
+         * used data items, one per mimetype. Then, within each mimetype, the list of data items
+         * for that type is also sorted, based off of {super primary, primary, times used} in that
+         * order.
          */
-        final List<Action> topActions = new ArrayList<>();
-        final List<Action> allActions = new ArrayList<>();
-        for (List<Action> mimeTypeActions : actions.values()) {
-            Collections.sort(mimeTypeActions, new Comparator<Action>() {
-                @Override
-                public int compare(Action lhs, Action rhs) {
-                    /*
-                     * Actions are compared to the same mimetype based off of three qualities:
-                     * 1. Super primary
-                     * 2. Primary
-                     * 3. Times used
-                     */
-                    if (lhs.isSuperPrimary()) {
-                        return -1;
-                    } else if (rhs.isSuperPrimary()) {
-                        return 1;
-                    } else if (lhs.isPrimary() && !rhs.isPrimary()) {
-                        return -1;
-                    } else if (!lhs.isPrimary() && rhs.isPrimary()) {
-                        return 1;
-                    } else {
-                        int lhsTimesUsed = lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed();
-                        int rhsTimesUsed = rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed();
+        final List<List<DataItem>> dataItemsList = new ArrayList<>();
+        for (List<DataItem> mimeTypeDataItems : dataItemsMap.values()) {
+            // Remove duplicate data items
+            Collapser.collapseList(mimeTypeDataItems, this);
+            // Sort within mimetype
+            Collections.sort(mimeTypeDataItems, mWithinMimeTypeDataItemComparator);
+            // Add to the list of data item lists
+            dataItemsList.add(mimeTypeDataItems);
+        }
+        Trace.endSection();
 
-                        return rhsTimesUsed - lhsTimesUsed;
-                    }
+        Trace.beginSection("sort amongst mimetypes");
+        // Sort amongst mimetypes to bubble up the top data items for the contact card
+        Collections.sort(dataItemsList, mAmongstMimeTypeDataItemComparator);
+        Trace.endSection();
+
+        return new Pair<>(dataItemsList, dataItemsMap);
+    }
+
+    /**
+     * Converts a {@link DataItem} into an {@link ExpandingEntryCardView.Entry} for display.
+     * If the {@link ExpandingEntryCardView.Entry} has no visual elements, null is returned.
+     * @param dataItem The {@link DataItem} to convert.
+     * @return The {@link ExpandingEntryCardView.Entry}, or null if no visual elements are present.
+     */
+    private Entry dataItemToEntry(DataItem dataItem) {
+        Drawable icon = null;
+        String header = null;
+        String subHeader = null;
+        Drawable subHeaderIcon = null;
+        String text = null;
+        Drawable textIcon = null;
+        Intent intent = null;
+        final boolean isEditable = false;
+
+        DataKind kind = dataItem.getDataKind();
+
+        if (dataItem instanceof ImDataItem) {
+            final ImDataItem im = (ImDataItem) dataItem;
+            intent = ContactsUtils.buildImIntent(this, im).first;
+            header = getResources().getString(R.string.header_im_entry);
+            final boolean isEmail = im.isCreatedFromEmail();
+            final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : im.getProtocol();
+            subHeader = Im.getProtocolLabel(getResources(), protocol,
+                    im.getCustomProtocol()).toString();
+        } else if (dataItem instanceof OrganizationDataItem) {
+            final OrganizationDataItem organization = (OrganizationDataItem) dataItem;
+            header = getResources().getString(R.string.header_organization_entry);
+            subHeader = organization.getCompany();
+            text = organization.getTitle();
+        } else if (dataItem instanceof NicknameDataItem) {
+            final NicknameDataItem nickname = (NicknameDataItem) dataItem;
+            // Build nickname entries
+            final boolean isNameRawContact =
+                (mContactData.getNameRawContactId() == dataItem.getRawContactId());
+
+            final boolean duplicatesTitle =
+                isNameRawContact
+                && mContactData.getDisplayNameSource() == DisplayNameSources.NICKNAME;
+
+            if (!duplicatesTitle) {
+                header = getResources().getString(R.string.header_nickname_entry);
+                subHeader = nickname.getName();
+            }
+        } else if (dataItem instanceof NoteDataItem) {
+            final NoteDataItem note = (NoteDataItem) dataItem;
+            header = getResources().getString(R.string.header_note_entry);
+            subHeader = note.getNote();
+        } else if (dataItem instanceof WebsiteDataItem) {
+            final WebsiteDataItem website = (WebsiteDataItem) dataItem;
+            header = getResources().getString(R.string.header_website_entry);
+            subHeader = website.getUrl();
+            try {
+                final WebAddress webAddress = new WebAddress(website.buildDataString(this, kind));
+                intent = new Intent(Intent.ACTION_VIEW, Uri.parse(webAddress.toString()));
+            } catch (final ParseException e) {
+                Log.e(TAG, "Couldn't parse website: " + website.buildDataString(this, kind));
+            }
+        } else if (dataItem instanceof EventDataItem) {
+            final EventDataItem event = (EventDataItem) dataItem;
+            final String dataString = event.buildDataString(this, kind);
+            final Calendar cal = DateUtils.parseDate(dataString, false);
+            if (cal != null) {
+                final Date nextAnniversary =
+                        DateUtils.getNextAnnualDate(cal);
+                final Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon();
+                builder.appendPath("time");
+                ContentUris.appendId(builder, nextAnniversary.getTime());
+                intent = new Intent(Intent.ACTION_VIEW).setData(builder.build());
+            }
+            header = getResources().getString(R.string.header_event_entry);
+            subHeader = getResources().getString(Event.getTypeResource(
+                    event.getKindTypeColumn(kind)));
+            text = DateUtils.formatDate(this, dataString);
+        } else if (dataItem instanceof RelationDataItem) {
+            final RelationDataItem relation = (RelationDataItem) dataItem;
+            final String dataString = relation.buildDataString(this, kind);
+            if (!TextUtils.isEmpty(dataString)) {
+                intent = new Intent(Intent.ACTION_SEARCH);
+                intent.putExtra(SearchManager.QUERY, dataString);
+                intent.setType(Contacts.CONTENT_TYPE);
+            }
+            header = getResources().getString(R.string.header_relation_entry);
+            subHeader = relation.getName();
+            text = Relation.getTypeLabel(getResources(), relation.getKindTypeColumn(kind),
+                    relation.getLabel()).toString();
+        } else if (dataItem instanceof PhoneDataItem) {
+            final PhoneDataItem phone = (PhoneDataItem) dataItem;
+            if (!TextUtils.isEmpty(phone.getNumber())) {
+                header = phone.buildDataString(this, kind);
+                text = Phone.getTypeLabel(getResources(), phone.getKindTypeColumn(kind),
+                        phone.getLabel()).toString();
+                icon = getResources().getDrawable(R.drawable.ic_phone_24dp);
+                if (PhoneCapabilityTester.isPhone(this)) {
+                    intent = CallUtil.getCallIntent(phone.getNumber());
                 }
-            });
-            topActions.add(mimeTypeActions.get(0));
-            // Add all the other actions and remove the top one
-            allActions.addAll(mimeTypeActions);
-            allActions.remove(mimeTypeActions.get(0));
+            }
+        } else if (dataItem instanceof EmailDataItem) {
+            final EmailDataItem email = (EmailDataItem) dataItem;
+            final String address = email.getData();
+            if (!TextUtils.isEmpty(address)) {
+                final Uri mailUri = Uri.fromParts(CallUtil.SCHEME_MAILTO, address, null);
+                intent = new Intent(Intent.ACTION_SENDTO, mailUri);
+                header = email.getAddress();
+                text = Email.getTypeLabel(getResources(), email.getKindTypeColumn(kind),
+                        email.getLabel()).toString();
+                icon = getResources().getDrawable(R.drawable.ic_email_24dp);
+            }
+        } else if (dataItem instanceof StructuredPostalDataItem) {
+            StructuredPostalDataItem postal = (StructuredPostalDataItem) dataItem;
+            final String postalAddress = postal.getFormattedAddress();
+            if (!TextUtils.isEmpty(postalAddress)) {
+                intent = StructuredPostalUtils.getViewPostalAddressIntent(postalAddress);
+                header = postal.getFormattedAddress();
+                text = StructuredPostal.getTypeLabel(getResources(), postal.getKindTypeColumn(kind),
+                        postal.getLabel()).toString();
+                icon = getResources().getDrawable(R.drawable.ic_place_24dp);
+            }
+        } else if (dataItem instanceof SipAddressDataItem) {
+            if (PhoneCapabilityTester.isSipPhone(this)) {
+                final SipAddressDataItem sip = (SipAddressDataItem) dataItem;
+                final String address = sip.getSipAddress();
+                if (!TextUtils.isEmpty(address)) {
+                    final Uri callUri = Uri.fromParts(CallUtil.SCHEME_SIP, address, null);
+                    intent = CallUtil.getCallIntent(callUri);
+                    // Note that this item will get a SIP-specific variant
+                    // of the "call phone" icon, rather than the standard
+                    // app icon for the Phone app (which we show for
+                    // regular phone numbers.)  That's because the phone
+                    // app explicitly specifies an android:icon attribute
+                    // for the SIP-related intent-filters in its manifest.
+                }
+                icon = ResolveCache.getInstance(this).getIcon(sip.getMimeType(), intent);
+                // Call mutate to create a new Drawable.ConstantState for color filtering
+                if (icon != null) {
+                    icon.mutate();
+                }
+            }
+        } else if (dataItem instanceof StructuredNameDataItem) {
+            final String givenName = ((StructuredNameDataItem) dataItem).getGivenName();
+            if (!TextUtils.isEmpty(givenName)) {
+                mAboutCard.setTitle(getResources().getString(R.string.about_card_title) +
+                        " " + givenName);
+            } else {
+                mAboutCard.setTitle(getResources().getString(R.string.about_card_title));
+            }
+        } else {
+            // Custom DataItem
+            header = dataItem.buildDataStringForDisplay(this, kind);
+            text = kind.typeColumn;
+            intent = new Intent(Intent.ACTION_VIEW);
+            intent.setDataAndType(Uri.parse(dataItem.buildDataString(this, kind)),
+                    dataItem.getMimeType());
+            icon = ResolveCache.getInstance(this).getIcon(dataItem.getMimeType(), intent);
         }
 
-        // topActions now contains the top action for each mimetype. This list now needs to be
-        // sorted, based off of {times used, last used, statically defined} in that order.
-        Collections.sort(topActions, new Comparator<Action>() {
-            @Override
-            public int compare(Action lhs, Action rhs) {
-                int lhsTimesUsed = lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed();
-                int rhsTimesUsed = rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed();
-                int timesUsedDifference = rhsTimesUsed - lhsTimesUsed;
-                if (timesUsedDifference != 0) {
-                    return timesUsedDifference;
-                }
-
-                long lhsLastTimeUsed = lhs.getLastTimeUsed() == null ? 0 : lhs.getLastTimeUsed();
-                long rhsLastTimeUsed = rhs.getLastTimeUsed() == null ? 0 : rhs.getLastTimeUsed();
-                long lastTimeUsedDifference = rhsLastTimeUsed - lhsLastTimeUsed;
-                if (lastTimeUsedDifference > 0) {
-                    return 1;
-                } else if (lastTimeUsedDifference < 0) {
-                    return -1;
-                }
-
-                // Times used and last time used are the same. Resort to statically defined.
-                String lhsMimeType = lhs.getMimeType();
-                String rhsMimeType = rhs.getMimeType();
-                for (String mimeType : LEADING_MIMETYPES) {
-                    if (lhsMimeType.equals(mimeType)) {
-                        return -1;
-                    } else if (rhsMimeType.equals(mimeType)) {
-                        return 1;
-                    }
-                }
-                // Trailing types come last, so flip the returns
-                for (String mimeType : TRAILING_MIMETYPES) {
-                    if (lhsMimeType.equals(mimeType)) {
-                        return 1;
-                    } else if (rhsMimeType.equals(mimeType)) {
-                        return -1;
-                    }
-                }
-                return 0;
+        if (intent != null) {
+            // Do not set the intent is there are no resolves
+            if (!PhoneCapabilityTester.isIntentRegistered(this, intent)) {
+                intent = null;
             }
-        });
+        }
 
-        entries.addAll(actionsToEntries(topActions));
-        entries.addAll(actionsToEntries(allActions));
-        Trace.endSection();
+        // If the Entry has no visual elements, return null
+        if (icon == null && TextUtils.isEmpty(header) && TextUtils.isEmpty(subHeader) &&
+                subHeaderIcon == null && TextUtils.isEmpty(text) && textIcon == null) {
+            return null;
+        }
+
+        return new Entry(icon, header, subHeader, subHeaderIcon, text, textIcon,
+                intent, isEditable);
+    }
+
+    private List<Entry> dataItemsToEntries(List<DataItem> dataItems) {
+        final List<Entry> entries = new ArrayList<>();
+        for (DataItem dataItem : dataItems) {
+            final Entry entry = dataItemToEntry(dataItem);
+            if (entry != null) {
+                entries.add(entry);
+            }
+        }
+        return entries;
     }
 
     /**
@@ -856,8 +1158,9 @@
 
         mColorFilter =
                 new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP);
-        mCommunicationCard.setColorAndFilter(color, mColorFilter);
+        mContactCard.setColorAndFilter(color, mColorFilter);
         mRecentCard.setColorAndFilter(color, mColorFilter);
+        mAboutCard.setColorAndFilter(color, mColorFilter);
     }
 
     private void updateStatusBarColor() {
@@ -891,65 +1194,8 @@
         return 0;
     }
 
-    /**
-     * Consider adding the given {@link Action}, which will only happen if
-     * {@link PackageManager} finds an application to handle
-     * {@link Action#getIntent()}.
-     * @param action the action to handle
-     * @param resolveCache cache of applications that can handle actions
-     * @param front indicates whether to add the action to the front of the list
-     * @param actions where to put the action.
-     * @return true if action has been added
-     */
-    private boolean considerAdd(Action action, ResolveCache resolveCache, boolean front,
-            ActionMultiMap actions) {
-        if (resolveCache.hasResolve(action)) {
-            actions.put(action.getMimeType(), action, front);
-            return true;
-        }
-        return false;
-    }
-
-    /**
-     * Converts a list of Action into a list of Entry
-     * @param actions The list of Action to convert
-     * @return The converted list of Entry
-     */
-    private List<Entry> actionsToEntries(List<Action> actions) {
-        List<Entry> entries = new ArrayList<>();
-        for (Action action :  actions) {
-            final String header = action.getBody() == null ? null : action.getBody().toString();
-            final String footer = action.getBody() == null ? null : action.getBody().toString();
-            String body = null;
-            Drawable icon = null;
-            switch (action.getMimeType()) {
-                case Phone.CONTENT_ITEM_TYPE:
-                    icon = getResources().getDrawable(R.drawable.ic_phone_24dp);
-                    break;
-                case Email.CONTENT_ITEM_TYPE:
-                    icon = getResources().getDrawable(R.drawable.ic_email_24dp);
-                    break;
-                case StructuredPostal.CONTENT_ITEM_TYPE:
-                    icon = getResources().getDrawable(R.drawable.ic_place_24dp);
-                    break;
-                default:
-                    icon = ResolveCache.getInstance(this).getIcon(action);
-            }
-            entries.add(new Entry(icon, header, body, footer, action.getIntent(),
-                    /* isEditable= */ false));
-
-            // Add SMS in addition to phone calls
-            if (action.getMimeType().equals(Phone.CONTENT_ITEM_TYPE)) {
-                entries.add(new Entry(getResources().getDrawable(R.drawable.ic_message_24dp),
-                        getResources().getString(R.string.send_message), null, header,
-                        action.getAlternateIntent(), /* isEditable = */ false));
-            }
-        }
-        return entries;
-    }
-
     private List<Entry> contactInteractionsToEntries(List<ContactInteraction> interactions) {
-        List<Entry> entries = new ArrayList<>();
+        final List<Entry> entries = new ArrayList<>();
         for (ContactInteraction interaction : interactions) {
             entries.add(new Entry(interaction.getIcon(this),
                     interaction.getViewHeader(this),
@@ -963,7 +1209,7 @@
         return entries;
     }
 
-    private LoaderCallbacks<Contact> mLoaderContactCallbacks =
+    private final LoaderCallbacks<Contact> mLoaderContactCallbacks =
             new LoaderCallbacks<Contact>() {
         @Override
         public void onLoaderReset(Loader<Contact> loader) {
@@ -1029,7 +1275,7 @@
         overridePendingTransition(0, 0);
     }
 
-    private LoaderCallbacks<List<ContactInteraction>> mLoaderInteractionsCallbacks =
+    private final LoaderCallbacks<List<ContactInteraction>> mLoaderInteractionsCallbacks =
             new LoaderCallbacks<List<ContactInteraction>>() {
 
         @Override
@@ -1091,7 +1337,7 @@
     }
 
     private void bindRecentData() {
-        List<ContactInteraction> allInteractions = new ArrayList<>();
+        final List<ContactInteraction> allInteractions = new ArrayList<>();
         for (List<ContactInteraction> loaderInteractions : mRecentLoaderResults.values()) {
             allInteractions.addAll(loaderInteractions);
         }
@@ -1110,6 +1356,15 @@
                     /* isExpanded = */ false, mExpandingEntryCardViewListener);
             mRecentCard.setVisibility(View.VISIBLE);
         }
+
+        // About card is initialized along with the contact card, but since it appears after
+        // the recent card in the UI, we hold off until making it visible until the recent card
+        // is also ready to avoid stuttering.
+        if (mAboutCard.shouldShow()) {
+            mAboutCard.setVisibility(View.VISIBLE);
+        } else {
+            mAboutCard.setVisibility(View.GONE);
+        }
     }
 
     @Override
@@ -1153,7 +1408,7 @@
                     !isStarred);
 
             // Now perform the real save
-            Intent intent = ContactSaveService.createSetStarredIntent(
+            final Intent intent = ContactSaveService.createSetStarredIntent(
                     QuickContactActivity.this, mLookupUri, !isStarred);
             startService(intent);
         }
@@ -1197,7 +1452,7 @@
 
         try {
             this.startActivity(chooseIntent);
-        } catch (ActivityNotFoundException ex) {
+        } catch (final ActivityNotFoundException ex) {
             Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show();
         }
     }
@@ -1229,7 +1484,7 @@
 
     @Override
     public boolean onCreateOptionsMenu(Menu menu) {
-        MenuInflater inflater = getMenuInflater();
+        final MenuInflater inflater = getMenuInflater();
         inflater.inflate(R.menu.quickcontact, menu);
         return true;
     }