Home | History | Annotate | Download | only in quickcontact
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.contacts.quickcontact;
     18 
     19 import android.accounts.Account;
     20 import android.animation.ArgbEvaluator;
     21 import android.animation.ObjectAnimator;
     22 import android.app.Activity;
     23 import android.app.Fragment;
     24 import android.app.LoaderManager.LoaderCallbacks;
     25 import android.app.SearchManager;
     26 import android.content.ActivityNotFoundException;
     27 import android.content.ContentUris;
     28 import android.content.ContentValues;
     29 import android.content.Context;
     30 import android.content.Intent;
     31 import android.content.Loader;
     32 import android.content.pm.PackageManager;
     33 import android.content.pm.ResolveInfo;
     34 import android.content.res.Resources;
     35 import android.graphics.Bitmap;
     36 import android.graphics.BitmapFactory;
     37 import android.graphics.Color;
     38 import android.graphics.PorterDuff;
     39 import android.graphics.PorterDuffColorFilter;
     40 import android.graphics.drawable.BitmapDrawable;
     41 import android.graphics.drawable.ColorDrawable;
     42 import android.graphics.drawable.Drawable;
     43 import android.net.Uri;
     44 import android.os.AsyncTask;
     45 import android.os.Bundle;
     46 import android.os.Trace;
     47 import android.provider.CalendarContract;
     48 import android.provider.ContactsContract.CommonDataKinds.Email;
     49 import android.provider.ContactsContract.CommonDataKinds.Event;
     50 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
     51 import android.provider.ContactsContract.CommonDataKinds.Identity;
     52 import android.provider.ContactsContract.CommonDataKinds.Im;
     53 import android.provider.ContactsContract.CommonDataKinds.Nickname;
     54 import android.provider.ContactsContract.CommonDataKinds.Note;
     55 import android.provider.ContactsContract.CommonDataKinds.Organization;
     56 import android.provider.ContactsContract.CommonDataKinds.Phone;
     57 import android.provider.ContactsContract.CommonDataKinds.Relation;
     58 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
     59 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
     60 import android.provider.ContactsContract.CommonDataKinds.Website;
     61 import android.provider.ContactsContract.Contacts;
     62 import android.provider.ContactsContract.Data;
     63 import android.provider.ContactsContract.Directory;
     64 import android.provider.ContactsContract.DisplayNameSources;
     65 import android.provider.ContactsContract.DataUsageFeedback;
     66 import android.provider.ContactsContract.Intents;
     67 import android.provider.ContactsContract.QuickContact;
     68 import android.provider.ContactsContract.RawContacts;
     69 import android.support.v7.graphics.Palette;
     70 import android.telecom.PhoneAccount;
     71 import android.telecom.TelecomManager;
     72 import android.text.BidiFormatter;
     73 import android.text.SpannableString;
     74 import android.text.TextDirectionHeuristics;
     75 import android.text.TextUtils;
     76 import android.util.Log;
     77 import android.view.ContextMenu;
     78 import android.view.ContextMenu.ContextMenuInfo;
     79 import android.view.Menu;
     80 import android.view.MenuInflater;
     81 import android.view.MenuItem;
     82 import android.view.MotionEvent;
     83 import android.view.View;
     84 import android.view.View.OnClickListener;
     85 import android.view.View.OnCreateContextMenuListener;
     86 import android.view.WindowManager;
     87 import android.widget.Toast;
     88 import android.widget.Toolbar;
     89 
     90 import com.android.contacts.ContactSaveService;
     91 import com.android.contacts.ContactsActivity;
     92 import com.android.contacts.NfcHandler;
     93 import com.android.contacts.R;
     94 import com.android.contacts.common.CallUtil;
     95 import com.android.contacts.common.ClipboardUtils;
     96 import com.android.contacts.common.Collapser;
     97 import com.android.contacts.common.ContactsUtils;
     98 import com.android.contacts.common.activity.RequestPermissionsActivity;
     99 import com.android.contacts.common.editor.SelectAccountDialogFragment;
    100 import com.android.contacts.common.interactions.TouchPointManager;
    101 import com.android.contacts.common.lettertiles.LetterTileDrawable;
    102 import com.android.contacts.common.list.ShortcutIntentBuilder;
    103 import com.android.contacts.common.list.ShortcutIntentBuilder.OnShortcutIntentCreatedListener;
    104 import com.android.contacts.common.model.AccountTypeManager;
    105 import com.android.contacts.common.model.Contact;
    106 import com.android.contacts.common.model.ContactLoader;
    107 import com.android.contacts.common.model.RawContact;
    108 import com.android.contacts.common.model.account.AccountType;
    109 import com.android.contacts.common.model.account.AccountWithDataSet;
    110 import com.android.contacts.common.model.dataitem.DataItem;
    111 import com.android.contacts.common.model.dataitem.DataKind;
    112 import com.android.contacts.common.model.dataitem.EmailDataItem;
    113 import com.android.contacts.common.model.dataitem.EventDataItem;
    114 import com.android.contacts.common.model.dataitem.ImDataItem;
    115 import com.android.contacts.common.model.dataitem.NicknameDataItem;
    116 import com.android.contacts.common.model.dataitem.NoteDataItem;
    117 import com.android.contacts.common.model.dataitem.OrganizationDataItem;
    118 import com.android.contacts.common.model.dataitem.PhoneDataItem;
    119 import com.android.contacts.common.model.dataitem.RelationDataItem;
    120 import com.android.contacts.common.model.dataitem.SipAddressDataItem;
    121 import com.android.contacts.common.model.dataitem.StructuredNameDataItem;
    122 import com.android.contacts.common.model.dataitem.StructuredPostalDataItem;
    123 import com.android.contacts.common.model.dataitem.WebsiteDataItem;
    124 import com.android.contacts.common.util.ImplicitIntentsUtil;
    125 import com.android.contacts.common.util.DateUtils;
    126 import com.android.contacts.common.util.MaterialColorMapUtils;
    127 import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette;
    128 import com.android.contacts.common.util.ViewUtil;
    129 import com.android.contacts.detail.ContactDisplayUtils;
    130 import com.android.contacts.editor.ContactEditorFragment;
    131 import com.android.contacts.editor.EditorIntents;
    132 import com.android.contacts.interactions.CalendarInteractionsLoader;
    133 import com.android.contacts.interactions.CallLogInteractionsLoader;
    134 import com.android.contacts.interactions.ContactDeletionInteraction;
    135 import com.android.contacts.interactions.ContactInteraction;
    136 import com.android.contacts.interactions.SmsInteractionsLoader;
    137 import com.android.contacts.quickcontact.ExpandingEntryCardView.Entry;
    138 import com.android.contacts.quickcontact.ExpandingEntryCardView.EntryContextMenuInfo;
    139 import com.android.contacts.quickcontact.ExpandingEntryCardView.EntryTag;
    140 import com.android.contacts.quickcontact.ExpandingEntryCardView.ExpandingEntryCardViewListener;
    141 import com.android.contacts.quickcontact.WebAddress.ParseException;
    142 import com.android.contacts.util.ImageViewDrawableSetter;
    143 import com.android.contacts.util.PhoneCapabilityTester;
    144 import com.android.contacts.util.SchedulingUtils;
    145 import com.android.contacts.util.StructuredPostalUtils;
    146 import com.android.contacts.widget.MultiShrinkScroller;
    147 import com.android.contacts.widget.MultiShrinkScroller.MultiShrinkScrollerListener;
    148 import com.android.contacts.widget.QuickContactImageView;
    149 import com.android.contactsbind.HelpUtils;
    150 
    151 import com.google.common.collect.Lists;
    152 
    153 import java.lang.SecurityException;
    154 import java.util.ArrayList;
    155 import java.util.Arrays;
    156 import java.util.Calendar;
    157 import java.util.Collections;
    158 import java.util.Comparator;
    159 import java.util.Date;
    160 import java.util.HashMap;
    161 import java.util.List;
    162 import java.util.Map;
    163 import java.util.concurrent.ConcurrentHashMap;
    164 
    165 /**
    166  * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads
    167  * data asynchronously, and then shows a popup with details centered around
    168  * {@link Intent#getSourceBounds()}.
    169  */
    170 public class QuickContactActivity extends ContactsActivity {
    171 
    172     /**
    173      * QuickContacts immediately takes up the full screen. All possible information is shown.
    174      * This value for {@link android.provider.ContactsContract.QuickContact#EXTRA_MODE}
    175      * should only be used by the Contacts app.
    176      */
    177     public static final int MODE_FULLY_EXPANDED = 4;
    178 
    179     private static final String TAG = "QuickContact";
    180 
    181     private static final String KEY_THEME_COLOR = "theme_color";
    182 
    183     private static final int ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION = 150;
    184     private static final int REQUEST_CODE_CONTACT_EDITOR_ACTIVITY = 1;
    185     private static final int SCRIM_COLOR = Color.argb(0xC8, 0, 0, 0);
    186     private static final int REQUEST_CODE_CONTACT_SELECTION_ACTIVITY = 2;
    187     private static final String MIMETYPE_SMS = "vnd.android-dir/mms-sms";
    188 
    189     /** This is the Intent action to install a shortcut in the launcher. */
    190     private static final String ACTION_INSTALL_SHORTCUT =
    191             "com.android.launcher.action.INSTALL_SHORTCUT";
    192 
    193     @SuppressWarnings("deprecation")
    194     private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY;
    195 
    196     private static final String MIMETYPE_GPLUS_PROFILE =
    197             "vnd.android.cursor.item/vnd.googleplus.profile";
    198     private static final String GPLUS_PROFILE_DATA_5_ADD_TO_CIRCLE = "addtocircle";
    199     private static final String GPLUS_PROFILE_DATA_5_VIEW_PROFILE = "view";
    200     private static final String MIMETYPE_HANGOUTS =
    201             "vnd.android.cursor.item/vnd.googleplus.profile.comm";
    202     private static final String HANGOUTS_DATA_5_VIDEO = "hangout";
    203     private static final String HANGOUTS_DATA_5_MESSAGE = "conversation";
    204     private static final String CALL_ORIGIN_QUICK_CONTACTS_ACTIVITY =
    205             "com.android.contacts.quickcontact.QuickContactActivity";
    206 
    207     /**
    208      * The URI used to load the the Contact. Once the contact is loaded, use Contact#getLookupUri()
    209      * instead of referencing this URI.
    210      */
    211     private Uri mLookupUri;
    212     private String[] mExcludeMimes;
    213     private int mExtraMode;
    214     private String mExtraPrioritizedMimeType;
    215     private int mStatusBarColor;
    216     private boolean mHasAlreadyBeenOpened;
    217     private boolean mOnlyOnePhoneNumber;
    218     private boolean mOnlyOneEmail;
    219 
    220     private QuickContactImageView mPhotoView;
    221     private ExpandingEntryCardView mContactCard;
    222     private ExpandingEntryCardView mNoContactDetailsCard;
    223     private ExpandingEntryCardView mRecentCard;
    224     private ExpandingEntryCardView mAboutCard;
    225     private MultiShrinkScroller mScroller;
    226     private SelectAccountDialogFragmentListener mSelectAccountFragmentListener;
    227     private AsyncTask<Void, Void, Cp2DataCardModel> mEntriesAndActionsTask;
    228     private AsyncTask<Void, Void, Void> mRecentDataTask;
    229     /**
    230      * The last copy of Cp2DataCardModel that was passed to {@link #populateContactAndAboutCard}.
    231      */
    232     private Cp2DataCardModel mCachedCp2DataCardModel;
    233     /**
    234      *  This scrim's opacity is controlled in two different ways. 1) Before the initial entrance
    235      *  animation finishes, the opacity is animated by a value animator. This is designed to
    236      *  distract the user from the length of the initial loading time. 2) After the initial
    237      *  entrance animation, the opacity is directly related to scroll position.
    238      */
    239     private ColorDrawable mWindowScrim;
    240     private boolean mIsEntranceAnimationFinished;
    241     private MaterialColorMapUtils mMaterialColorMapUtils;
    242     private boolean mIsExitAnimationInProgress;
    243     private boolean mHasComputedThemeColor;
    244 
    245     /**
    246      * Used to stop the ExpandingEntry cards from adjusting between an entry click and the intent
    247      * being launched.
    248      */
    249     private boolean mHasIntentLaunched;
    250 
    251     private Contact mContactData;
    252     private ContactLoader mContactLoader;
    253     private PorterDuffColorFilter mColorFilter;
    254     private int mColorFilterColor;
    255 
    256     private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter();
    257 
    258     /**
    259      * {@link #LEADING_MIMETYPES} is used to sort MIME-types.
    260      *
    261      * <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog,
    262      * in the order specified here.</p>
    263      */
    264     private static final List<String> LEADING_MIMETYPES = Lists.newArrayList(
    265             Phone.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE,
    266             StructuredPostal.CONTENT_ITEM_TYPE);
    267 
    268     private static final List<String> SORTED_ABOUT_CARD_MIMETYPES = Lists.newArrayList(
    269             Nickname.CONTENT_ITEM_TYPE,
    270             // Phonetic name is inserted after nickname if it is available.
    271             // No mimetype for phonetic name exists.
    272             Website.CONTENT_ITEM_TYPE,
    273             Organization.CONTENT_ITEM_TYPE,
    274             Event.CONTENT_ITEM_TYPE,
    275             Relation.CONTENT_ITEM_TYPE,
    276             Im.CONTENT_ITEM_TYPE,
    277             GroupMembership.CONTENT_ITEM_TYPE,
    278             Identity.CONTENT_ITEM_TYPE,
    279             Note.CONTENT_ITEM_TYPE);
    280 
    281     private static final BidiFormatter sBidiFormatter = BidiFormatter.getInstance();
    282 
    283     /** Id for the background contact loader */
    284     private static final int LOADER_CONTACT_ID = 0;
    285 
    286     private static final String KEY_LOADER_EXTRA_PHONES =
    287             QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_PHONES";
    288 
    289     /** Id for the background Sms Loader */
    290     private static final int LOADER_SMS_ID = 1;
    291     private static final int MAX_SMS_RETRIEVE = 3;
    292 
    293     /** Id for the back Calendar Loader */
    294     private static final int LOADER_CALENDAR_ID = 2;
    295     private static final String KEY_LOADER_EXTRA_EMAILS =
    296             QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_EMAILS";
    297     private static final int MAX_PAST_CALENDAR_RETRIEVE = 3;
    298     private static final int MAX_FUTURE_CALENDAR_RETRIEVE = 3;
    299     private static final long PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR =
    300             1L * 24L * 60L * 60L * 1000L /* 1 day */;
    301     private static final long FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR =
    302             7L * 24L * 60L * 60L * 1000L /* 7 days */;
    303 
    304     /** Id for the background Call Log Loader */
    305     private static final int LOADER_CALL_LOG_ID = 3;
    306     private static final int MAX_CALL_LOG_RETRIEVE = 3;
    307     private static final int MIN_NUM_CONTACT_ENTRIES_SHOWN = 3;
    308     private static final int MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN = 3;
    309     private static final int CARD_ENTRY_ID_EDIT_CONTACT = -2;
    310 
    311 
    312     private static final int[] mRecentLoaderIds = new int[]{
    313         LOADER_SMS_ID,
    314         LOADER_CALENDAR_ID,
    315         LOADER_CALL_LOG_ID};
    316     /**
    317      * ConcurrentHashMap constructor params: 4 is initial table size, 0.9f is
    318      * load factor before resizing, 1 means we only expect a single thread to
    319      * write to the map so make only a single shard
    320      */
    321     private Map<Integer, List<ContactInteraction>> mRecentLoaderResults =
    322         new ConcurrentHashMap<>(4, 0.9f, 1);
    323 
    324     private static final String FRAGMENT_TAG_SELECT_ACCOUNT = "select_account_fragment";
    325 
    326     final OnClickListener mEntryClickHandler = new OnClickListener() {
    327         @Override
    328         public void onClick(View v) {
    329             final Object entryTagObject = v.getTag();
    330             if (entryTagObject == null || !(entryTagObject instanceof EntryTag)) {
    331                 Log.w(TAG, "EntryTag was not used correctly");
    332                 return;
    333             }
    334             final EntryTag entryTag = (EntryTag) entryTagObject;
    335             final Intent intent = entryTag.getIntent();
    336             final int dataId = entryTag.getId();
    337 
    338             if (dataId == CARD_ENTRY_ID_EDIT_CONTACT) {
    339                 editContact();
    340                 return;
    341             }
    342 
    343             // Pass the touch point through the intent for use in the InCallUI
    344             if (Intent.ACTION_CALL.equals(intent.getAction())) {
    345                 if (TouchPointManager.getInstance().hasValidPoint()) {
    346                     Bundle extras = new Bundle();
    347                     extras.putParcelable(TouchPointManager.TOUCH_POINT,
    348                             TouchPointManager.getInstance().getPoint());
    349                     intent.putExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extras);
    350                 }
    351             }
    352 
    353             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    354 
    355             mHasIntentLaunched = true;
    356             try {
    357                 startActivity(intent);
    358             } catch (SecurityException ex) {
    359                 Toast.makeText(QuickContactActivity.this, R.string.missing_app,
    360                         Toast.LENGTH_SHORT).show();
    361                 Log.e(TAG, "QuickContacts does not have permission to launch "
    362                         + intent);
    363             } catch (ActivityNotFoundException ex) {
    364                 Toast.makeText(QuickContactActivity.this, R.string.missing_app,
    365                         Toast.LENGTH_SHORT).show();
    366             }
    367 
    368             // Default to USAGE_TYPE_CALL. Usage is summed among all types for sorting each data id
    369             // so the exact usage type is not necessary in all cases
    370             String usageType = DataUsageFeedback.USAGE_TYPE_CALL;
    371 
    372             final Uri intentUri = intent.getData();
    373             if ((intentUri != null && intentUri.getScheme() != null &&
    374                     intentUri.getScheme().equals(ContactsUtils.SCHEME_SMSTO)) ||
    375                     (intent.getType() != null && intent.getType().equals(MIMETYPE_SMS))) {
    376                 usageType = DataUsageFeedback.USAGE_TYPE_SHORT_TEXT;
    377             }
    378 
    379             // Data IDs start at 1 so anything less is invalid
    380             if (dataId > 0) {
    381                 final Uri dataUsageUri = DataUsageFeedback.FEEDBACK_URI.buildUpon()
    382                         .appendPath(String.valueOf(dataId))
    383                         .appendQueryParameter(DataUsageFeedback.USAGE_TYPE, usageType)
    384                         .build();
    385                 try {
    386                     final boolean successful = getContentResolver().update(
    387                             dataUsageUri, new ContentValues(), null, null) > 0;
    388                     if (!successful) {
    389                         Log.w(TAG, "DataUsageFeedback increment failed");
    390                     }
    391                 } catch (SecurityException ex) {
    392                     Log.w(TAG, "DataUsageFeedback increment failed", ex);
    393                 }
    394             } else {
    395                 Log.w(TAG, "Invalid Data ID");
    396             }
    397 
    398             // Pass the touch point through the intent for use in the InCallUI
    399             if (Intent.ACTION_CALL.equals(intent.getAction())) {
    400                 if (TouchPointManager.getInstance().hasValidPoint()) {
    401                     Bundle extras = new Bundle();
    402                     extras.putParcelable(TouchPointManager.TOUCH_POINT,
    403                             TouchPointManager.getInstance().getPoint());
    404                     intent.putExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extras);
    405                 }
    406             }
    407 
    408             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    409 
    410             mHasIntentLaunched = true;
    411             try {
    412                 ImplicitIntentsUtil.startActivityInAppIfPossible(QuickContactActivity.this, intent);
    413             } catch (SecurityException ex) {
    414                 Toast.makeText(QuickContactActivity.this, R.string.missing_app,
    415                         Toast.LENGTH_SHORT).show();
    416                 Log.e(TAG, "QuickContacts does not have permission to launch "
    417                         + intent);
    418             } catch (ActivityNotFoundException ex) {
    419                 Toast.makeText(QuickContactActivity.this, R.string.missing_app,
    420                         Toast.LENGTH_SHORT).show();
    421             }
    422         }
    423     };
    424 
    425     final ExpandingEntryCardViewListener mExpandingEntryCardViewListener
    426             = new ExpandingEntryCardViewListener() {
    427         @Override
    428         public void onCollapse(int heightDelta) {
    429             mScroller.prepareForShrinkingScrollChild(heightDelta);
    430         }
    431 
    432         @Override
    433         public void onExpand() {
    434             mScroller.setDisableTouchesForSuppressLayout(/* areTouchesDisabled = */ true);
    435         }
    436 
    437         @Override
    438         public void onExpandDone() {
    439             mScroller.setDisableTouchesForSuppressLayout(/* areTouchesDisabled = */ false);
    440         }
    441     };
    442 
    443     private interface ContextMenuIds {
    444         static final int COPY_TEXT = 0;
    445         static final int CLEAR_DEFAULT = 1;
    446         static final int SET_DEFAULT = 2;
    447     }
    448 
    449     private final OnCreateContextMenuListener mEntryContextMenuListener =
    450             new OnCreateContextMenuListener() {
    451         @Override
    452         public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
    453             if (menuInfo == null) {
    454                 return;
    455             }
    456             final EntryContextMenuInfo info = (EntryContextMenuInfo) menuInfo;
    457             menu.setHeaderTitle(info.getCopyText());
    458             menu.add(ContextMenu.NONE, ContextMenuIds.COPY_TEXT,
    459                     ContextMenu.NONE, getString(R.string.copy_text));
    460 
    461             // Don't allow setting or clearing of defaults for non-editable contacts
    462             if (!isContactEditable()) {
    463                 return;
    464             }
    465 
    466             final String selectedMimeType = info.getMimeType();
    467 
    468             // Defaults to true will only enable the detail to be copied to the clipboard.
    469             boolean onlyOneOfMimeType = true;
    470 
    471             // Only allow primary support for Phone and Email content types
    472             if (Phone.CONTENT_ITEM_TYPE.equals(selectedMimeType)) {
    473                 onlyOneOfMimeType = mOnlyOnePhoneNumber;
    474             } else if (Email.CONTENT_ITEM_TYPE.equals(selectedMimeType)) {
    475                 onlyOneOfMimeType = mOnlyOneEmail;
    476             }
    477 
    478             // Checking for previously set default
    479             if (info.isSuperPrimary()) {
    480                 menu.add(ContextMenu.NONE, ContextMenuIds.CLEAR_DEFAULT,
    481                         ContextMenu.NONE, getString(R.string.clear_default));
    482             } else if (!onlyOneOfMimeType) {
    483                 menu.add(ContextMenu.NONE, ContextMenuIds.SET_DEFAULT,
    484                         ContextMenu.NONE, getString(R.string.set_default));
    485             }
    486         }
    487     };
    488 
    489     @Override
    490     public boolean onContextItemSelected(MenuItem item) {
    491         EntryContextMenuInfo menuInfo;
    492         try {
    493             menuInfo = (EntryContextMenuInfo) item.getMenuInfo();
    494         } catch (ClassCastException e) {
    495             Log.e(TAG, "bad menuInfo", e);
    496             return false;
    497         }
    498 
    499         switch (item.getItemId()) {
    500             case ContextMenuIds.COPY_TEXT:
    501                 ClipboardUtils.copyText(this, menuInfo.getCopyLabel(), menuInfo.getCopyText(),
    502                         true);
    503                 return true;
    504             case ContextMenuIds.SET_DEFAULT:
    505                 final Intent setIntent = ContactSaveService.createSetSuperPrimaryIntent(this,
    506                         menuInfo.getId());
    507                 this.startService(setIntent);
    508                 return true;
    509             case ContextMenuIds.CLEAR_DEFAULT:
    510                 final Intent clearIntent = ContactSaveService.createClearPrimaryIntent(this,
    511                         menuInfo.getId());
    512                 this.startService(clearIntent);
    513                 return true;
    514             default:
    515                 throw new IllegalArgumentException("Unknown menu option " + item.getItemId());
    516         }
    517     }
    518 
    519     /**
    520      * Headless fragment used to handle account selection callbacks invoked from
    521      * {@link DirectoryContactUtil}.
    522      */
    523     public static class SelectAccountDialogFragmentListener extends Fragment
    524             implements SelectAccountDialogFragment.Listener {
    525 
    526         private QuickContactActivity mQuickContactActivity;
    527 
    528         public SelectAccountDialogFragmentListener() {}
    529 
    530         @Override
    531         public void onAccountChosen(AccountWithDataSet account, Bundle extraArgs) {
    532             DirectoryContactUtil.createCopy(mQuickContactActivity.mContactData.getContentValues(),
    533                     account, mQuickContactActivity);
    534         }
    535 
    536         @Override
    537         public void onAccountSelectorCancelled() {}
    538 
    539         /**
    540          * Set the parent activity. Since rotation can cause this fragment to be used across
    541          * more than one activity instance, we need to explicitly set this value instead
    542          * of making this class non-static.
    543          */
    544         public void setQuickContactActivity(QuickContactActivity quickContactActivity) {
    545             mQuickContactActivity = quickContactActivity;
    546         }
    547     }
    548 
    549     final MultiShrinkScrollerListener mMultiShrinkScrollerListener
    550             = new MultiShrinkScrollerListener() {
    551         @Override
    552         public void onScrolledOffBottom() {
    553             finish();
    554         }
    555 
    556         @Override
    557         public void onEnterFullscreen() {
    558             updateStatusBarColor();
    559         }
    560 
    561         @Override
    562         public void onExitFullscreen() {
    563             updateStatusBarColor();
    564         }
    565 
    566         @Override
    567         public void onStartScrollOffBottom() {
    568             mIsExitAnimationInProgress = true;
    569         }
    570 
    571         @Override
    572         public void onEntranceAnimationDone() {
    573             mIsEntranceAnimationFinished = true;
    574         }
    575 
    576         @Override
    577         public void onTransparentViewHeightChange(float ratio) {
    578             if (mIsEntranceAnimationFinished) {
    579                 mWindowScrim.setAlpha((int) (0xFF * ratio));
    580             }
    581         }
    582     };
    583 
    584 
    585     /**
    586      * Data items are compared to the same mimetype based off of three qualities:
    587      * 1. Super primary
    588      * 2. Primary
    589      * 3. Times used
    590      */
    591     private final Comparator<DataItem> mWithinMimeTypeDataItemComparator =
    592             new Comparator<DataItem>() {
    593         @Override
    594         public int compare(DataItem lhs, DataItem rhs) {
    595             if (!lhs.getMimeType().equals(rhs.getMimeType())) {
    596                 Log.wtf(TAG, "Comparing DataItems with different mimetypes lhs.getMimeType(): " +
    597                         lhs.getMimeType() + " rhs.getMimeType(): " + rhs.getMimeType());
    598                 return 0;
    599             }
    600 
    601             if (lhs.isSuperPrimary()) {
    602                 return -1;
    603             } else if (rhs.isSuperPrimary()) {
    604                 return 1;
    605             } else if (lhs.isPrimary() && !rhs.isPrimary()) {
    606                 return -1;
    607             } else if (!lhs.isPrimary() && rhs.isPrimary()) {
    608                 return 1;
    609             } else {
    610                 final int lhsTimesUsed =
    611                         lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed();
    612                 final int rhsTimesUsed =
    613                         rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed();
    614 
    615                 return rhsTimesUsed - lhsTimesUsed;
    616             }
    617         }
    618     };
    619 
    620     /**
    621      * Sorts among different mimetypes based off:
    622      * 1. Whether one of the mimetypes is the prioritized mimetype
    623      * 2. Number of times used
    624      * 3. Last time used
    625      * 4. Statically defined
    626      */
    627     private final Comparator<List<DataItem>> mAmongstMimeTypeDataItemComparator =
    628             new Comparator<List<DataItem>> () {
    629         @Override
    630         public int compare(List<DataItem> lhsList, List<DataItem> rhsList) {
    631             final DataItem lhs = lhsList.get(0);
    632             final DataItem rhs = rhsList.get(0);
    633             final String lhsMimeType = lhs.getMimeType();
    634             final String rhsMimeType = rhs.getMimeType();
    635 
    636             // 1. Whether one of the mimetypes is the prioritized mimetype
    637             if (!TextUtils.isEmpty(mExtraPrioritizedMimeType) && !lhsMimeType.equals(rhsMimeType)) {
    638                 if (rhsMimeType.equals(mExtraPrioritizedMimeType)) {
    639                     return 1;
    640                 }
    641                 if (lhsMimeType.equals(mExtraPrioritizedMimeType)) {
    642                     return -1;
    643                 }
    644             }
    645 
    646             // 2. Number of times used
    647             final int lhsTimesUsed = lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed();
    648             final int rhsTimesUsed = rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed();
    649             final int timesUsedDifference = rhsTimesUsed - lhsTimesUsed;
    650             if (timesUsedDifference != 0) {
    651                 return timesUsedDifference;
    652             }
    653 
    654             // 3. Last time used
    655             final long lhsLastTimeUsed =
    656                     lhs.getLastTimeUsed() == null ? 0 : lhs.getLastTimeUsed();
    657             final long rhsLastTimeUsed =
    658                     rhs.getLastTimeUsed() == null ? 0 : rhs.getLastTimeUsed();
    659             final long lastTimeUsedDifference = rhsLastTimeUsed - lhsLastTimeUsed;
    660             if (lastTimeUsedDifference > 0) {
    661                 return 1;
    662             } else if (lastTimeUsedDifference < 0) {
    663                 return -1;
    664             }
    665 
    666             // 4. Resort to a statically defined mimetype order.
    667             if (!lhsMimeType.equals(rhsMimeType)) {
    668                 for (String mimeType : LEADING_MIMETYPES) {
    669                     if (lhsMimeType.equals(mimeType)) {
    670                         return -1;
    671                     } else if (rhsMimeType.equals(mimeType)) {
    672                         return 1;
    673                     }
    674                 }
    675             }
    676             return 0;
    677         }
    678     };
    679 
    680     @Override
    681     public boolean dispatchTouchEvent(MotionEvent ev) {
    682         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    683             TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY());
    684         }
    685         return super.dispatchTouchEvent(ev);
    686     }
    687 
    688     @Override
    689     protected void onCreate(Bundle savedInstanceState) {
    690         Trace.beginSection("onCreate()");
    691         super.onCreate(savedInstanceState);
    692 
    693         if (RequestPermissionsActivity.startPermissionActivity(this)) {
    694             return;
    695         }
    696 
    697         getWindow().setStatusBarColor(Color.TRANSPARENT);
    698 
    699         processIntent(getIntent());
    700 
    701         // Show QuickContact in front of soft input
    702         getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
    703                 WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
    704 
    705         setContentView(R.layout.quickcontact_activity);
    706 
    707         mMaterialColorMapUtils = new MaterialColorMapUtils(getResources());
    708 
    709         mScroller = (MultiShrinkScroller) findViewById(R.id.multiscroller);
    710 
    711         mContactCard = (ExpandingEntryCardView) findViewById(R.id.communication_card);
    712         mNoContactDetailsCard = (ExpandingEntryCardView) findViewById(R.id.no_contact_data_card);
    713         mRecentCard = (ExpandingEntryCardView) findViewById(R.id.recent_card);
    714         mAboutCard = (ExpandingEntryCardView) findViewById(R.id.about_card);
    715 
    716         mNoContactDetailsCard.setOnClickListener(mEntryClickHandler);
    717         mContactCard.setOnClickListener(mEntryClickHandler);
    718         mContactCard.setExpandButtonText(
    719         getResources().getString(R.string.expanding_entry_card_view_see_all));
    720         mContactCard.setOnCreateContextMenuListener(mEntryContextMenuListener);
    721 
    722         mRecentCard.setOnClickListener(mEntryClickHandler);
    723         mRecentCard.setTitle(getResources().getString(R.string.recent_card_title));
    724 
    725         mAboutCard.setOnClickListener(mEntryClickHandler);
    726         mAboutCard.setOnCreateContextMenuListener(mEntryContextMenuListener);
    727 
    728         mPhotoView = (QuickContactImageView) findViewById(R.id.photo);
    729         final View transparentView = findViewById(R.id.transparent_view);
    730         if (mScroller != null) {
    731             transparentView.setOnClickListener(new OnClickListener() {
    732                 @Override
    733                 public void onClick(View v) {
    734                     mScroller.scrollOffBottom();
    735                 }
    736             });
    737         }
    738 
    739         // Allow a shadow to be shown under the toolbar.
    740         ViewUtil.addRectangularOutlineProvider(findViewById(R.id.toolbar_parent), getResources());
    741 
    742         final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    743         setActionBar(toolbar);
    744         getActionBar().setTitle(null);
    745         // Put a TextView with a known resource id into the ActionBar. This allows us to easily
    746         // find the correct TextView location & size later.
    747         toolbar.addView(getLayoutInflater().inflate(R.layout.quickcontact_title_placeholder, null));
    748 
    749         mHasAlreadyBeenOpened = savedInstanceState != null;
    750         mIsEntranceAnimationFinished = mHasAlreadyBeenOpened;
    751         mWindowScrim = new ColorDrawable(SCRIM_COLOR);
    752         mWindowScrim.setAlpha(0);
    753         getWindow().setBackgroundDrawable(mWindowScrim);
    754 
    755         mScroller.initialize(mMultiShrinkScrollerListener, mExtraMode == MODE_FULLY_EXPANDED);
    756         // mScroller needs to perform asynchronous measurements after initalize(), therefore
    757         // we can't mark this as GONE.
    758         mScroller.setVisibility(View.INVISIBLE);
    759 
    760         setHeaderNameText(R.string.missing_name);
    761 
    762         mSelectAccountFragmentListener= (SelectAccountDialogFragmentListener) getFragmentManager()
    763                 .findFragmentByTag(FRAGMENT_TAG_SELECT_ACCOUNT);
    764         if (mSelectAccountFragmentListener == null) {
    765             mSelectAccountFragmentListener = new SelectAccountDialogFragmentListener();
    766             getFragmentManager().beginTransaction().add(0, mSelectAccountFragmentListener,
    767                     FRAGMENT_TAG_SELECT_ACCOUNT).commit();
    768             mSelectAccountFragmentListener.setRetainInstance(true);
    769         }
    770         mSelectAccountFragmentListener.setQuickContactActivity(this);
    771 
    772         SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ true,
    773                 new Runnable() {
    774                     @Override
    775                     public void run() {
    776                         if (!mHasAlreadyBeenOpened) {
    777                             // The initial scrim opacity must match the scrim opacity that would be
    778                             // achieved by scrolling to the starting position.
    779                             final float alphaRatio = mExtraMode == MODE_FULLY_EXPANDED ?
    780                                     1 : mScroller.getStartingTransparentHeightRatio();
    781                             final int duration = getResources().getInteger(
    782                                     android.R.integer.config_shortAnimTime);
    783                             final int desiredAlpha = (int) (0xFF * alphaRatio);
    784                             ObjectAnimator o = ObjectAnimator.ofInt(mWindowScrim, "alpha", 0,
    785                                     desiredAlpha).setDuration(duration);
    786 
    787                             o.start();
    788                         }
    789                     }
    790                 });
    791 
    792         if (savedInstanceState != null) {
    793             final int color = savedInstanceState.getInt(KEY_THEME_COLOR, 0);
    794             SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false,
    795                     new Runnable() {
    796                         @Override
    797                         public void run() {
    798                             // Need to wait for the pre draw before setting the initial scroll
    799                             // value. Prior to pre draw all scroll values are invalid.
    800                             if (mHasAlreadyBeenOpened) {
    801                                 mScroller.setVisibility(View.VISIBLE);
    802                                 mScroller.setScroll(mScroller.getScrollNeededToBeFullScreen());
    803                             }
    804                             // Need to wait for pre draw for setting the theme color. Setting the
    805                             // header tint before the MultiShrinkScroller has been measured will
    806                             // cause incorrect tinting calculations.
    807                             if (color != 0) {
    808                                 setThemeColor(mMaterialColorMapUtils
    809                                         .calculatePrimaryAndSecondaryColor(color));
    810                             }
    811                         }
    812                     });
    813         }
    814 
    815         Trace.endSection();
    816     }
    817 
    818     @Override
    819     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    820         if (requestCode == REQUEST_CODE_CONTACT_EDITOR_ACTIVITY &&
    821                 resultCode == ContactDeletionInteraction.RESULT_CODE_DELETED) {
    822             // The contact that we were showing has been deleted.
    823             finish();
    824         } else if (requestCode == REQUEST_CODE_CONTACT_SELECTION_ACTIVITY &&
    825                 resultCode != RESULT_CANCELED) {
    826             processIntent(data);
    827         }
    828     }
    829 
    830     @Override
    831     protected void onNewIntent(Intent intent) {
    832         super.onNewIntent(intent);
    833         mHasAlreadyBeenOpened = true;
    834         mIsEntranceAnimationFinished = true;
    835         mHasComputedThemeColor = false;
    836         processIntent(intent);
    837     }
    838 
    839     @Override
    840     public void onSaveInstanceState(Bundle savedInstanceState) {
    841         super.onSaveInstanceState(savedInstanceState);
    842         if (mColorFilter != null) {
    843             savedInstanceState.putInt(KEY_THEME_COLOR, mColorFilterColor);
    844         }
    845     }
    846 
    847     private void processIntent(Intent intent) {
    848         if (intent == null) {
    849             finish();
    850             return;
    851         }
    852         Uri lookupUri = intent.getData();
    853 
    854         // Check to see whether it comes from the old version.
    855         if (lookupUri != null && LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) {
    856             final long rawContactId = ContentUris.parseId(lookupUri);
    857             lookupUri = RawContacts.getContactLookupUri(getContentResolver(),
    858                     ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
    859         }
    860         mExtraMode = getIntent().getIntExtra(QuickContact.EXTRA_MODE, QuickContact.MODE_LARGE);
    861         mExtraPrioritizedMimeType = getIntent().getStringExtra(QuickContact.EXTRA_PRIORITIZED_MIMETYPE);
    862         final Uri oldLookupUri = mLookupUri;
    863 
    864         if (lookupUri == null) {
    865             finish();
    866             return;
    867         }
    868         mLookupUri = lookupUri;
    869         mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES);
    870         if (oldLookupUri == null) {
    871             mContactLoader = (ContactLoader) getLoaderManager().initLoader(
    872                     LOADER_CONTACT_ID, null, mLoaderContactCallbacks);
    873         } else if (oldLookupUri != mLookupUri) {
    874             // After copying a directory contact, the contact URI changes. Therefore,
    875             // we need to restart the loader and reload the new contact.
    876             destroyInteractionLoaders();
    877             mContactLoader = (ContactLoader) getLoaderManager().restartLoader(
    878                     LOADER_CONTACT_ID, null, mLoaderContactCallbacks);
    879             mCachedCp2DataCardModel = null;
    880         }
    881 
    882         NfcHandler.register(this, mLookupUri);
    883     }
    884 
    885     private void destroyInteractionLoaders() {
    886         for (int interactionLoaderId : mRecentLoaderIds) {
    887             getLoaderManager().destroyLoader(interactionLoaderId);
    888         }
    889     }
    890 
    891     private void runEntranceAnimation() {
    892         if (mHasAlreadyBeenOpened) {
    893             return;
    894         }
    895         mHasAlreadyBeenOpened = true;
    896         mScroller.scrollUpForEntranceAnimation(mExtraMode != MODE_FULLY_EXPANDED);
    897     }
    898 
    899     /** Assign this string to the view if it is not empty. */
    900     private void setHeaderNameText(int resId) {
    901         if (mScroller != null) {
    902             mScroller.setTitle(getText(resId) == null ? null : getText(resId).toString());
    903         }
    904     }
    905 
    906     /** Assign this string to the view if it is not empty. */
    907     private void setHeaderNameText(String value) {
    908         if (!TextUtils.isEmpty(value)) {
    909             if (mScroller != null) {
    910                 mScroller.setTitle(value);
    911             }
    912         }
    913     }
    914 
    915     /**
    916      * Check if the given MIME-type appears in the list of excluded MIME-types
    917      * that the most-recent caller requested.
    918      */
    919     private boolean isMimeExcluded(String mimeType) {
    920         if (mExcludeMimes == null) return false;
    921         for (String excludedMime : mExcludeMimes) {
    922             if (TextUtils.equals(excludedMime, mimeType)) {
    923                 return true;
    924             }
    925         }
    926         return false;
    927     }
    928 
    929     /**
    930      * Handle the result from the ContactLoader
    931      */
    932     private void bindContactData(final Contact data) {
    933         Trace.beginSection("bindContactData");
    934         mContactData = data;
    935         invalidateOptionsMenu();
    936 
    937         Trace.endSection();
    938         Trace.beginSection("Set display photo & name");
    939 
    940         mPhotoView.setIsBusiness(mContactData.isDisplayNameFromOrganization());
    941         mPhotoSetter.setupContactPhoto(data, mPhotoView);
    942         extractAndApplyTintFromPhotoViewAsynchronously();
    943         String phoneticName = ContactDisplayUtils.getPhoneticName(this, data);
    944         String displayName = ContactDisplayUtils.getDisplayName(this, data).toString();
    945         if (mContactData.getDisplayNameSource() != DisplayNameSources.STRUCTURED_PHONETIC_NAME
    946                 && !TextUtils.isEmpty(phoneticName)) {
    947             displayName = getResources().getString(
    948                     R.string.quick_contact_display_name_with_phonetic, displayName, phoneticName);
    949             setHeaderNameText(displayName);
    950         } else {
    951             setHeaderNameText(displayName);
    952         }
    953 
    954         Trace.endSection();
    955 
    956         mEntriesAndActionsTask = new AsyncTask<Void, Void, Cp2DataCardModel>() {
    957 
    958             @Override
    959             protected Cp2DataCardModel doInBackground(
    960                     Void... params) {
    961                 return generateDataModelFromContact(data);
    962             }
    963 
    964             @Override
    965             protected void onPostExecute(Cp2DataCardModel cardDataModel) {
    966                 super.onPostExecute(cardDataModel);
    967                 // Check that original AsyncTask parameters are still valid and the activity
    968                 // is still running before binding to UI. A new intent could invalidate
    969                 // the results, for example.
    970                 if (data == mContactData && !isCancelled()) {
    971                     bindDataToCards(cardDataModel);
    972                     showActivity();
    973                 }
    974             }
    975         };
    976         mEntriesAndActionsTask.execute();
    977     }
    978 
    979     private void bindDataToCards(Cp2DataCardModel cp2DataCardModel) {
    980         startInteractionLoaders(cp2DataCardModel);
    981         populateContactAndAboutCard(cp2DataCardModel);
    982     }
    983 
    984     private void startInteractionLoaders(Cp2DataCardModel cp2DataCardModel) {
    985         final Map<String, List<DataItem>> dataItemsMap = cp2DataCardModel.dataItemsMap;
    986         final List<DataItem> phoneDataItems = dataItemsMap.get(Phone.CONTENT_ITEM_TYPE);
    987         if (phoneDataItems != null && phoneDataItems.size() == 1) {
    988             mOnlyOnePhoneNumber = true;
    989         }
    990         String[] phoneNumbers = null;
    991         if (phoneDataItems != null) {
    992             phoneNumbers = new String[phoneDataItems.size()];
    993             for (int i = 0; i < phoneDataItems.size(); ++i) {
    994                 phoneNumbers[i] = ((PhoneDataItem) phoneDataItems.get(i)).getNumber();
    995             }
    996         }
    997         final Bundle phonesExtraBundle = new Bundle();
    998         phonesExtraBundle.putStringArray(KEY_LOADER_EXTRA_PHONES, phoneNumbers);
    999 
   1000         Trace.beginSection("start sms loader");
   1001         getLoaderManager().initLoader(
   1002                 LOADER_SMS_ID,
   1003                 phonesExtraBundle,
   1004                 mLoaderInteractionsCallbacks);
   1005         Trace.endSection();
   1006 
   1007         Trace.beginSection("start call log loader");
   1008         getLoaderManager().initLoader(
   1009                 LOADER_CALL_LOG_ID,
   1010                 phonesExtraBundle,
   1011                 mLoaderInteractionsCallbacks);
   1012         Trace.endSection();
   1013 
   1014 
   1015         Trace.beginSection("start calendar loader");
   1016         final List<DataItem> emailDataItems = dataItemsMap.get(Email.CONTENT_ITEM_TYPE);
   1017         if (emailDataItems != null && emailDataItems.size() == 1) {
   1018             mOnlyOneEmail = true;
   1019         }
   1020         String[] emailAddresses = null;
   1021         if (emailDataItems != null) {
   1022             emailAddresses = new String[emailDataItems.size()];
   1023             for (int i = 0; i < emailDataItems.size(); ++i) {
   1024                 emailAddresses[i] = ((EmailDataItem) emailDataItems.get(i)).getAddress();
   1025             }
   1026         }
   1027         final Bundle emailsExtraBundle = new Bundle();
   1028         emailsExtraBundle.putStringArray(KEY_LOADER_EXTRA_EMAILS, emailAddresses);
   1029         getLoaderManager().initLoader(
   1030                 LOADER_CALENDAR_ID,
   1031                 emailsExtraBundle,
   1032                 mLoaderInteractionsCallbacks);
   1033         Trace.endSection();
   1034     }
   1035 
   1036     private void showActivity() {
   1037         if (mScroller != null) {
   1038             mScroller.setVisibility(View.VISIBLE);
   1039             SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false,
   1040                     new Runnable() {
   1041                         @Override
   1042                         public void run() {
   1043                             runEntranceAnimation();
   1044                         }
   1045                     });
   1046         }
   1047     }
   1048 
   1049     private List<List<Entry>> buildAboutCardEntries(Map<String, List<DataItem>> dataItemsMap) {
   1050         final List<List<Entry>> aboutCardEntries = new ArrayList<>();
   1051         for (String mimetype : SORTED_ABOUT_CARD_MIMETYPES) {
   1052             final List<DataItem> mimeTypeItems = dataItemsMap.get(mimetype);
   1053             if (mimeTypeItems == null) {
   1054                 continue;
   1055             }
   1056             // Set aboutCardTitleOut = null, since SORTED_ABOUT_CARD_MIMETYPES doesn't contain
   1057             // the name mimetype.
   1058             final List<Entry> aboutEntries = dataItemsToEntries(mimeTypeItems,
   1059                     /* aboutCardTitleOut = */ null);
   1060             if (aboutEntries.size() > 0) {
   1061                 aboutCardEntries.add(aboutEntries);
   1062             }
   1063         }
   1064         return aboutCardEntries;
   1065     }
   1066 
   1067     @Override
   1068     protected void onResume() {
   1069         super.onResume();
   1070         // If returning from a launched activity, repopulate the contact and about card
   1071         if (mHasIntentLaunched) {
   1072             mHasIntentLaunched = false;
   1073             populateContactAndAboutCard(mCachedCp2DataCardModel);
   1074         }
   1075 
   1076         // When exiting the activity and resuming, we want to force a full reload of all the
   1077         // interaction data in case something changed in the background. On screen rotation,
   1078         // we don't need to do this. And, mCachedCp2DataCardModel will be null, so we won't.
   1079         if (mCachedCp2DataCardModel != null) {
   1080             destroyInteractionLoaders();
   1081             startInteractionLoaders(mCachedCp2DataCardModel);
   1082         }
   1083     }
   1084 
   1085     private void populateContactAndAboutCard(Cp2DataCardModel cp2DataCardModel) {
   1086         mCachedCp2DataCardModel = cp2DataCardModel;
   1087         if (mHasIntentLaunched || cp2DataCardModel == null) {
   1088             return;
   1089         }
   1090         Trace.beginSection("bind contact card");
   1091 
   1092         final List<List<Entry>> contactCardEntries = cp2DataCardModel.contactCardEntries;
   1093         final List<List<Entry>> aboutCardEntries = cp2DataCardModel.aboutCardEntries;
   1094         final String customAboutCardName = cp2DataCardModel.customAboutCardName;
   1095 
   1096         if (contactCardEntries.size() > 0) {
   1097             final boolean firstEntriesArePrioritizedMimeType =
   1098                     !TextUtils.isEmpty(mExtraPrioritizedMimeType) &&
   1099                     mCachedCp2DataCardModel.dataItemsMap.containsKey(mExtraPrioritizedMimeType) &&
   1100                     mCachedCp2DataCardModel.dataItemsMap.get(mExtraPrioritizedMimeType).size() != 0;
   1101             mContactCard.initialize(contactCardEntries,
   1102                     /* numInitialVisibleEntries = */ MIN_NUM_CONTACT_ENTRIES_SHOWN,
   1103                     /* isExpanded = */ mContactCard.isExpanded(),
   1104                     /* isAlwaysExpanded = */ false,
   1105                     mExpandingEntryCardViewListener,
   1106                     mScroller,
   1107                     firstEntriesArePrioritizedMimeType);
   1108             mContactCard.setVisibility(View.VISIBLE);
   1109         } else {
   1110             mContactCard.setVisibility(View.GONE);
   1111         }
   1112         Trace.endSection();
   1113 
   1114         Trace.beginSection("bind about card");
   1115         // Phonetic name is not a data item, so the entry needs to be created separately
   1116         final String phoneticName = mContactData.getPhoneticName();
   1117         if (!TextUtils.isEmpty(phoneticName)) {
   1118             Entry phoneticEntry = new Entry(/* viewId = */ -1,
   1119                     /* icon = */ null,
   1120                     getResources().getString(R.string.name_phonetic),
   1121                     phoneticName,
   1122                     /* subHeaderIcon = */ null,
   1123                     /* text = */ null,
   1124                     /* textIcon = */ null,
   1125                     /* primaryContentDescription = */ null,
   1126                     /* intent = */ null,
   1127                     /* alternateIcon = */ null,
   1128                     /* alternateIntent = */ null,
   1129                     /* alternateContentDescription = */ null,
   1130                     /* shouldApplyColor = */ false,
   1131                     /* isEditable = */ false,
   1132                     /* EntryContextMenuInfo = */ new EntryContextMenuInfo(phoneticName,
   1133                             getResources().getString(R.string.name_phonetic),
   1134                             /* mimeType = */ null, /* id = */ -1, /* isPrimary = */ false),
   1135                     /* thirdIcon = */ null,
   1136                     /* thirdIntent = */ null,
   1137                     /* thirdContentDescription = */ null,
   1138                     /* iconResourceId = */ 0);
   1139             List<Entry> phoneticList = new ArrayList<>();
   1140             phoneticList.add(phoneticEntry);
   1141             // Phonetic name comes after nickname. Check to see if the first entry type is nickname
   1142             if (aboutCardEntries.size() > 0 && aboutCardEntries.get(0).get(0).getHeader().equals(
   1143                     getResources().getString(R.string.header_nickname_entry))) {
   1144                 aboutCardEntries.add(1, phoneticList);
   1145             } else {
   1146                 aboutCardEntries.add(0, phoneticList);
   1147             }
   1148         }
   1149 
   1150         if (!TextUtils.isEmpty(customAboutCardName)) {
   1151             mAboutCard.setTitle(customAboutCardName);
   1152         }
   1153 
   1154         mAboutCard.initialize(aboutCardEntries,
   1155                 /* numInitialVisibleEntries = */ 1,
   1156                 /* isExpanded = */ true,
   1157                 /* isAlwaysExpanded = */ true,
   1158                 mExpandingEntryCardViewListener,
   1159                 mScroller);
   1160 
   1161         if (contactCardEntries.size() == 0 && aboutCardEntries.size() == 0) {
   1162             initializeNoContactDetailCard();
   1163         } else {
   1164             mNoContactDetailsCard.setVisibility(View.GONE);
   1165         }
   1166 
   1167         // If the Recent card is already initialized (all recent data is loaded), show the About
   1168         // card if it has entries. Otherwise About card visibility will be set in bindRecentData()
   1169         if (isAllRecentDataLoaded() && aboutCardEntries.size() > 0) {
   1170             mAboutCard.setVisibility(View.VISIBLE);
   1171         }
   1172         Trace.endSection();
   1173     }
   1174 
   1175     /**
   1176      * Create a card that shows "Add email" and "Add phone number" entries in grey.
   1177      */
   1178     private void initializeNoContactDetailCard() {
   1179         final Drawable phoneIcon = getResources().getDrawable(
   1180                 R.drawable.ic_phone_24dp).mutate();
   1181         final Entry phonePromptEntry = new Entry(CARD_ENTRY_ID_EDIT_CONTACT,
   1182                 phoneIcon, getString(R.string.quickcontact_add_phone_number),
   1183                 /* subHeader = */ null, /* subHeaderIcon = */ null, /* text = */ null,
   1184                 /* textIcon = */ null, /* primaryContentDescription = */ null,
   1185                 getEditContactIntent(),
   1186                 /* alternateIcon = */ null, /* alternateIntent = */ null,
   1187                 /* alternateContentDescription = */ null, /* shouldApplyColor = */ true,
   1188                 /* isEditable = */ false, /* EntryContextMenuInfo = */ null,
   1189                 /* thirdIcon = */ null, /* thirdIntent = */ null,
   1190                 /* thirdContentDescription = */ null, R.drawable.ic_phone_24dp);
   1191 
   1192         final Drawable emailIcon = getResources().getDrawable(
   1193                 R.drawable.ic_email_24dp).mutate();
   1194         final Entry emailPromptEntry = new Entry(CARD_ENTRY_ID_EDIT_CONTACT,
   1195                 emailIcon, getString(R.string.quickcontact_add_email), /* subHeader = */ null,
   1196                 /* subHeaderIcon = */ null,
   1197                 /* text = */ null, /* textIcon = */ null, /* primaryContentDescription = */ null,
   1198                 getEditContactIntent(), /* alternateIcon = */ null,
   1199                 /* alternateIntent = */ null, /* alternateContentDescription = */ null,
   1200                 /* shouldApplyColor = */ true, /* isEditable = */ false,
   1201                 /* EntryContextMenuInfo = */ null, /* thirdIcon = */ null,
   1202                 /* thirdIntent = */ null, /* thirdContentDescription = */ null,
   1203                 R.drawable.ic_email_24dp);
   1204 
   1205         final List<List<Entry>> promptEntries = new ArrayList<>();
   1206         promptEntries.add(new ArrayList<Entry>(1));
   1207         promptEntries.add(new ArrayList<Entry>(1));
   1208         promptEntries.get(0).add(phonePromptEntry);
   1209         promptEntries.get(1).add(emailPromptEntry);
   1210 
   1211         final int subHeaderTextColor = getResources().getColor(
   1212                 R.color.quickcontact_entry_sub_header_text_color);
   1213         final PorterDuffColorFilter greyColorFilter =
   1214                 new PorterDuffColorFilter(subHeaderTextColor, PorterDuff.Mode.SRC_ATOP);
   1215         mNoContactDetailsCard.initialize(promptEntries, 2, /* isExpanded = */ true,
   1216                 /* isAlwaysExpanded = */ true, mExpandingEntryCardViewListener, mScroller);
   1217         mNoContactDetailsCard.setVisibility(View.VISIBLE);
   1218         mNoContactDetailsCard.setEntryHeaderColor(subHeaderTextColor);
   1219         mNoContactDetailsCard.setColorAndFilter(subHeaderTextColor, greyColorFilter);
   1220     }
   1221 
   1222     /**
   1223      * Builds the {@link DataItem}s Map out of the Contact.
   1224      * @param data The contact to build the data from.
   1225      * @return A pair containing a list of data items sorted within mimetype and sorted
   1226      *  amongst mimetype. The map goes from mimetype string to the sorted list of data items within
   1227      *  mimetype
   1228      */
   1229     private Cp2DataCardModel generateDataModelFromContact(
   1230             Contact data) {
   1231         Trace.beginSection("Build data items map");
   1232 
   1233         final Map<String, List<DataItem>> dataItemsMap = new HashMap<>();
   1234 
   1235         final ResolveCache cache = ResolveCache.getInstance(this);
   1236         for (RawContact rawContact : data.getRawContacts()) {
   1237             for (DataItem dataItem : rawContact.getDataItems()) {
   1238                 dataItem.setRawContactId(rawContact.getId());
   1239 
   1240                 final String mimeType = dataItem.getMimeType();
   1241                 if (mimeType == null) continue;
   1242 
   1243                 final AccountType accountType = rawContact.getAccountType(this);
   1244                 final DataKind dataKind = AccountTypeManager.getInstance(this)
   1245                         .getKindOrFallback(accountType, mimeType);
   1246                 if (dataKind == null) continue;
   1247 
   1248                 dataItem.setDataKind(dataKind);
   1249 
   1250                 final boolean hasData = !TextUtils.isEmpty(dataItem.buildDataString(this,
   1251                         dataKind));
   1252 
   1253                 if (isMimeExcluded(mimeType) || !hasData) continue;
   1254 
   1255                 List<DataItem> dataItemListByType = dataItemsMap.get(mimeType);
   1256                 if (dataItemListByType == null) {
   1257                     dataItemListByType = new ArrayList<>();
   1258                     dataItemsMap.put(mimeType, dataItemListByType);
   1259                 }
   1260                 dataItemListByType.add(dataItem);
   1261             }
   1262         }
   1263         Trace.endSection();
   1264 
   1265         Trace.beginSection("sort within mimetypes");
   1266         /*
   1267          * Sorting is a multi part step. The end result is to a have a sorted list of the most
   1268          * used data items, one per mimetype. Then, within each mimetype, the list of data items
   1269          * for that type is also sorted, based off of {super primary, primary, times used} in that
   1270          * order.
   1271          */
   1272         final List<List<DataItem>> dataItemsList = new ArrayList<>();
   1273         for (List<DataItem> mimeTypeDataItems : dataItemsMap.values()) {
   1274             // Remove duplicate data items
   1275             Collapser.collapseList(mimeTypeDataItems, this);
   1276             // Sort within mimetype
   1277             Collections.sort(mimeTypeDataItems, mWithinMimeTypeDataItemComparator);
   1278             // Add to the list of data item lists
   1279             dataItemsList.add(mimeTypeDataItems);
   1280         }
   1281         Trace.endSection();
   1282 
   1283         Trace.beginSection("sort amongst mimetypes");
   1284         // Sort amongst mimetypes to bubble up the top data items for the contact card
   1285         Collections.sort(dataItemsList, mAmongstMimeTypeDataItemComparator);
   1286         Trace.endSection();
   1287 
   1288         Trace.beginSection("cp2 data items to entries");
   1289 
   1290         final List<List<Entry>> contactCardEntries = new ArrayList<>();
   1291         final List<List<Entry>> aboutCardEntries = buildAboutCardEntries(dataItemsMap);
   1292         final MutableString aboutCardName = new MutableString();
   1293 
   1294         for (int i = 0; i < dataItemsList.size(); ++i) {
   1295             final List<DataItem> dataItemsByMimeType = dataItemsList.get(i);
   1296             final DataItem topDataItem = dataItemsByMimeType.get(0);
   1297             if (SORTED_ABOUT_CARD_MIMETYPES.contains(topDataItem.getMimeType())) {
   1298                 // About card mimetypes are built in buildAboutCardEntries, skip here
   1299                 continue;
   1300             } else {
   1301                 List<Entry> contactEntries = dataItemsToEntries(dataItemsList.get(i),
   1302                         aboutCardName);
   1303                 if (contactEntries.size() > 0) {
   1304                     contactCardEntries.add(contactEntries);
   1305                 }
   1306             }
   1307         }
   1308 
   1309         Trace.endSection();
   1310 
   1311         final Cp2DataCardModel dataModel = new Cp2DataCardModel();
   1312         dataModel.customAboutCardName = aboutCardName.value;
   1313         dataModel.aboutCardEntries = aboutCardEntries;
   1314         dataModel.contactCardEntries = contactCardEntries;
   1315         dataModel.dataItemsMap = dataItemsMap;
   1316         return dataModel;
   1317     }
   1318 
   1319     /**
   1320      * Class used to hold the About card and Contact cards' data model that gets generated
   1321      * on a background thread. All data is from CP2.
   1322      */
   1323     private static class Cp2DataCardModel {
   1324         /**
   1325          * A map between a mimetype string and the corresponding list of data items. The data items
   1326          * are in sorted order using mWithinMimeTypeDataItemComparator.
   1327          */
   1328         public Map<String, List<DataItem>> dataItemsMap;
   1329         public List<List<Entry>> aboutCardEntries;
   1330         public List<List<Entry>> contactCardEntries;
   1331         public String customAboutCardName;
   1332     }
   1333 
   1334     private static class MutableString {
   1335         public String value;
   1336     }
   1337 
   1338     /**
   1339      * Converts a {@link DataItem} into an {@link ExpandingEntryCardView.Entry} for display.
   1340      * If the {@link ExpandingEntryCardView.Entry} has no visual elements, null is returned.
   1341      *
   1342      * This runs on a background thread. This is set as static to avoid accidentally adding
   1343      * additional dependencies on unsafe things (like the Activity).
   1344      *
   1345      * @param dataItem The {@link DataItem} to convert.
   1346      * @param secondDataItem A second {@link DataItem} to help build a full entry for some
   1347      *  mimetypes
   1348      * @return The {@link ExpandingEntryCardView.Entry}, or null if no visual elements are present.
   1349      */
   1350     private static Entry dataItemToEntry(DataItem dataItem, DataItem secondDataItem,
   1351             Context context, Contact contactData,
   1352             final MutableString aboutCardName) {
   1353         Drawable icon = null;
   1354         String header = null;
   1355         String subHeader = null;
   1356         Drawable subHeaderIcon = null;
   1357         String text = null;
   1358         Drawable textIcon = null;
   1359         StringBuilder primaryContentDescription = new StringBuilder();
   1360         Intent intent = null;
   1361         boolean shouldApplyColor = true;
   1362         Drawable alternateIcon = null;
   1363         Intent alternateIntent = null;
   1364         StringBuilder alternateContentDescription = new StringBuilder();
   1365         final boolean isEditable = false;
   1366         EntryContextMenuInfo entryContextMenuInfo = null;
   1367         Drawable thirdIcon = null;
   1368         Intent thirdIntent = null;
   1369         String thirdContentDescription = null;
   1370         int iconResourceId = 0;
   1371 
   1372         context = context.getApplicationContext();
   1373         final Resources res = context.getResources();
   1374         DataKind kind = dataItem.getDataKind();
   1375 
   1376         if (dataItem instanceof ImDataItem) {
   1377             final ImDataItem im = (ImDataItem) dataItem;
   1378             intent = ContactsUtils.buildImIntent(context, im).first;
   1379             final boolean isEmail = im.isCreatedFromEmail();
   1380             final int protocol;
   1381             if (!im.isProtocolValid()) {
   1382                 protocol = Im.PROTOCOL_CUSTOM;
   1383             } else {
   1384                 protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : im.getProtocol();
   1385             }
   1386             if (protocol == Im.PROTOCOL_CUSTOM) {
   1387                 // If the protocol is custom, display the "IM" entry header as well to distinguish
   1388                 // this entry from other ones
   1389                 header = res.getString(R.string.header_im_entry);
   1390                 subHeader = Im.getProtocolLabel(res, protocol,
   1391                         im.getCustomProtocol()).toString();
   1392                 text = im.getData();
   1393             } else {
   1394                 header = Im.getProtocolLabel(res, protocol,
   1395                         im.getCustomProtocol()).toString();
   1396                 subHeader = im.getData();
   1397             }
   1398             entryContextMenuInfo = new EntryContextMenuInfo(im.getData(), header,
   1399                     dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary());
   1400         } else if (dataItem instanceof OrganizationDataItem) {
   1401             final OrganizationDataItem organization = (OrganizationDataItem) dataItem;
   1402             header = res.getString(R.string.header_organization_entry);
   1403             subHeader = organization.getCompany();
   1404             entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header,
   1405                     dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary());
   1406             text = organization.getTitle();
   1407         } else if (dataItem instanceof NicknameDataItem) {
   1408             final NicknameDataItem nickname = (NicknameDataItem) dataItem;
   1409             // Build nickname entries
   1410             final boolean isNameRawContact =
   1411                 (contactData.getNameRawContactId() == dataItem.getRawContactId());
   1412 
   1413             final boolean duplicatesTitle =
   1414                 isNameRawContact
   1415                 && contactData.getDisplayNameSource() == DisplayNameSources.NICKNAME;
   1416 
   1417             if (!duplicatesTitle) {
   1418                 header = res.getString(R.string.header_nickname_entry);
   1419                 subHeader = nickname.getName();
   1420                 entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header,
   1421                         dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary());
   1422             }
   1423         } else if (dataItem instanceof NoteDataItem) {
   1424             final NoteDataItem note = (NoteDataItem) dataItem;
   1425             header = res.getString(R.string.header_note_entry);
   1426             subHeader = note.getNote();
   1427             entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header,
   1428                     dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary());
   1429         } else if (dataItem instanceof WebsiteDataItem) {
   1430             final WebsiteDataItem website = (WebsiteDataItem) dataItem;
   1431             header = res.getString(R.string.header_website_entry);
   1432             subHeader = website.getUrl();
   1433             entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header,
   1434                     dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary());
   1435             try {
   1436                 final WebAddress webAddress = new WebAddress(website.buildDataStringForDisplay
   1437                         (context, kind));
   1438                 intent = new Intent(Intent.ACTION_VIEW, Uri.parse(webAddress.toString()));
   1439             } catch (final ParseException e) {
   1440                 Log.e(TAG, "Couldn't parse website: " + website.buildDataStringForDisplay(
   1441                         context, kind));
   1442             }
   1443         } else if (dataItem instanceof EventDataItem) {
   1444             final EventDataItem event = (EventDataItem) dataItem;
   1445             final String dataString = event.buildDataStringForDisplay(context, kind);
   1446             final Calendar cal = DateUtils.parseDate(dataString, false);
   1447             if (cal != null) {
   1448                 final Date nextAnniversary =
   1449                         DateUtils.getNextAnnualDate(cal);
   1450                 final Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon();
   1451                 builder.appendPath("time");
   1452                 ContentUris.appendId(builder, nextAnniversary.getTime());
   1453                 intent = new Intent(Intent.ACTION_VIEW).setData(builder.build());
   1454             }
   1455             header = res.getString(R.string.header_event_entry);
   1456             if (event.hasKindTypeColumn(kind)) {
   1457                 subHeader = Event.getTypeLabel(res, event.getKindTypeColumn(kind),
   1458                         event.getLabel()).toString();
   1459             }
   1460             text = DateUtils.formatDate(context, dataString);
   1461             entryContextMenuInfo = new EntryContextMenuInfo(text, header,
   1462                     dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary());
   1463         } else if (dataItem instanceof RelationDataItem) {
   1464             final RelationDataItem relation = (RelationDataItem) dataItem;
   1465             final String dataString = relation.buildDataStringForDisplay(context, kind);
   1466             if (!TextUtils.isEmpty(dataString)) {
   1467                 intent = new Intent(Intent.ACTION_SEARCH);
   1468                 intent.putExtra(SearchManager.QUERY, dataString);
   1469                 intent.setType(Contacts.CONTENT_TYPE);
   1470             }
   1471             header = res.getString(R.string.header_relation_entry);
   1472             subHeader = relation.getName();
   1473             entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header,
   1474                     dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary());
   1475             if (relation.hasKindTypeColumn(kind)) {
   1476                 text = Relation.getTypeLabel(res,
   1477                         relation.getKindTypeColumn(kind),
   1478                         relation.getLabel()).toString();
   1479             }
   1480         } else if (dataItem instanceof PhoneDataItem) {
   1481             final PhoneDataItem phone = (PhoneDataItem) dataItem;
   1482             if (!TextUtils.isEmpty(phone.getNumber())) {
   1483                 primaryContentDescription.append(res.getString(R.string.call_other)).append(" ");
   1484                 header = sBidiFormatter.unicodeWrap(phone.buildDataStringForDisplay(context, kind),
   1485                         TextDirectionHeuristics.LTR);
   1486                 entryContextMenuInfo = new EntryContextMenuInfo(header,
   1487                         res.getString(R.string.phoneLabelsGroup), dataItem.getMimeType(),
   1488                         dataItem.getId(), dataItem.isSuperPrimary());
   1489                 if (phone.hasKindTypeColumn(kind)) {
   1490                     final int kindTypeColumn = phone.getKindTypeColumn(kind);
   1491                     final String label = phone.getLabel();
   1492                     if (kindTypeColumn == Phone.TYPE_CUSTOM && TextUtils.isEmpty(label)) {
   1493                         text = "";
   1494                     } else {
   1495                         text = Phone.getTypeLabel(res, kindTypeColumn, label).toString();
   1496                         primaryContentDescription.append(text).append(" ");
   1497                     }
   1498                 }
   1499                 primaryContentDescription.append(header);
   1500                 icon = res.getDrawable(R.drawable.ic_phone_24dp);
   1501                 iconResourceId = R.drawable.ic_phone_24dp;
   1502                 if (PhoneCapabilityTester.isPhone(context)) {
   1503                     intent = CallUtil.getCallIntent(phone.getNumber());
   1504                 }
   1505                 alternateIntent = new Intent(Intent.ACTION_SENDTO,
   1506                         Uri.fromParts(ContactsUtils.SCHEME_SMSTO, phone.getNumber(), null));
   1507 
   1508                 alternateIcon = res.getDrawable(R.drawable.ic_message_24dp);
   1509                 alternateContentDescription.append(res.getString(R.string.sms_custom, header));
   1510 
   1511                 // Add video call button if supported
   1512                 if (CallUtil.isVideoEnabled(context)) {
   1513                     thirdIcon = res.getDrawable(R.drawable.ic_videocam);
   1514                     thirdIntent = CallUtil.getVideoCallIntent(phone.getNumber(),
   1515                             CALL_ORIGIN_QUICK_CONTACTS_ACTIVITY);
   1516                     thirdContentDescription =
   1517                             res.getString(R.string.description_video_call);
   1518                 }
   1519             }
   1520         } else if (dataItem instanceof EmailDataItem) {
   1521             final EmailDataItem email = (EmailDataItem) dataItem;
   1522             final String address = email.getData();
   1523             if (!TextUtils.isEmpty(address)) {
   1524                 primaryContentDescription.append(res.getString(R.string.email_other)).append(" ");
   1525                 final Uri mailUri = Uri.fromParts(ContactsUtils.SCHEME_MAILTO, address, null);
   1526                 intent = new Intent(Intent.ACTION_SENDTO, mailUri);
   1527                 header = email.getAddress();
   1528                 entryContextMenuInfo = new EntryContextMenuInfo(header,
   1529                         res.getString(R.string.emailLabelsGroup), dataItem.getMimeType(),
   1530                         dataItem.getId(), dataItem.isSuperPrimary());
   1531                 if (email.hasKindTypeColumn(kind)) {
   1532                     text = Email.getTypeLabel(res, email.getKindTypeColumn(kind),
   1533                             email.getLabel()).toString();
   1534                     primaryContentDescription.append(text).append(" ");
   1535                 }
   1536                 primaryContentDescription.append(header);
   1537                 icon = res.getDrawable(R.drawable.ic_email_24dp);
   1538                 iconResourceId = R.drawable.ic_email_24dp;
   1539             }
   1540         } else if (dataItem instanceof StructuredPostalDataItem) {
   1541             StructuredPostalDataItem postal = (StructuredPostalDataItem) dataItem;
   1542             final String postalAddress = postal.getFormattedAddress();
   1543             if (!TextUtils.isEmpty(postalAddress)) {
   1544                 primaryContentDescription.append(res.getString(R.string.map_other)).append(" ");
   1545                 intent = StructuredPostalUtils.getViewPostalAddressIntent(postalAddress);
   1546                 header = postal.getFormattedAddress();
   1547                 entryContextMenuInfo = new EntryContextMenuInfo(header,
   1548                         res.getString(R.string.postalLabelsGroup), dataItem.getMimeType(),
   1549                         dataItem.getId(), dataItem.isSuperPrimary());
   1550                 if (postal.hasKindTypeColumn(kind)) {
   1551                     text = StructuredPostal.getTypeLabel(res,
   1552                             postal.getKindTypeColumn(kind), postal.getLabel()).toString();
   1553                     primaryContentDescription.append(text).append(" ");
   1554                 }
   1555                 primaryContentDescription.append(header);
   1556                 alternateIntent =
   1557                         StructuredPostalUtils.getViewPostalAddressDirectionsIntent(postalAddress);
   1558                 alternateIcon = res.getDrawable(R.drawable.ic_directions_24dp);
   1559                 alternateContentDescription.append(res.getString(
   1560                         R.string.content_description_directions)).append(" ").append(header);
   1561                 icon = res.getDrawable(R.drawable.ic_place_24dp);
   1562                 iconResourceId = R.drawable.ic_place_24dp;
   1563             }
   1564         } else if (dataItem instanceof SipAddressDataItem) {
   1565             final SipAddressDataItem sip = (SipAddressDataItem) dataItem;
   1566             final String address = sip.getSipAddress();
   1567             if (!TextUtils.isEmpty(address)) {
   1568                 primaryContentDescription.append(res.getString(R.string.call_other)).append(
   1569                         " ");
   1570                 if (PhoneCapabilityTester.isSipPhone(context)) {
   1571                     final Uri callUri = Uri.fromParts(PhoneAccount.SCHEME_SIP, address, null);
   1572                     intent = CallUtil.getCallIntent(callUri);
   1573                 }
   1574                 header = address;
   1575                 entryContextMenuInfo = new EntryContextMenuInfo(header,
   1576                         res.getString(R.string.phoneLabelsGroup), dataItem.getMimeType(),
   1577                         dataItem.getId(), dataItem.isSuperPrimary());
   1578                 if (sip.hasKindTypeColumn(kind)) {
   1579                     text = SipAddress.getTypeLabel(res,
   1580                             sip.getKindTypeColumn(kind), sip.getLabel()).toString();
   1581                     primaryContentDescription.append(text).append(" ");
   1582                 }
   1583                 primaryContentDescription.append(header);
   1584                 icon = res.getDrawable(R.drawable.ic_dialer_sip_black_24dp);
   1585                 iconResourceId = R.drawable.ic_dialer_sip_black_24dp;
   1586             }
   1587         } else if (dataItem instanceof StructuredNameDataItem) {
   1588             // If the name is already set and this is not the super primary value then leave the
   1589             // current value. This way we show the super primary value when we are able to.
   1590             if (dataItem.isSuperPrimary() || aboutCardName.value == null
   1591                     || aboutCardName.value.isEmpty()) {
   1592                 final String givenName = ((StructuredNameDataItem) dataItem).getGivenName();
   1593                 if (!TextUtils.isEmpty(givenName)) {
   1594                     aboutCardName.value = res.getString(R.string.about_card_title) +
   1595                             " " + givenName;
   1596                 } else {
   1597                     aboutCardName.value = res.getString(R.string.about_card_title);
   1598                 }
   1599             }
   1600         } else {
   1601             // Custom DataItem
   1602             header = dataItem.buildDataStringForDisplay(context, kind);
   1603             text = kind.typeColumn;
   1604             intent = new Intent(Intent.ACTION_VIEW);
   1605             final Uri uri = ContentUris.withAppendedId(Data.CONTENT_URI, dataItem.getId());
   1606             intent.setDataAndType(uri, dataItem.getMimeType());
   1607 
   1608             if (intent != null) {
   1609                 final String mimetype = intent.getType();
   1610 
   1611                 // Build advanced entry for known 3p types. Otherwise default to ResolveCache icon.
   1612                 switch (mimetype) {
   1613                     case MIMETYPE_GPLUS_PROFILE:
   1614                         // If a secondDataItem is available, use it to build an entry with
   1615                         // alternate actions
   1616                         if (secondDataItem != null) {
   1617                             icon = res.getDrawable(R.drawable.ic_google_plus_24dp);
   1618                             alternateIcon = res.getDrawable(R.drawable.ic_add_to_circles_black_24);
   1619                             final GPlusOrHangoutsDataItemModel itemModel =
   1620                                     new GPlusOrHangoutsDataItemModel(intent, alternateIntent,
   1621                                             dataItem, secondDataItem, alternateContentDescription,
   1622                                             header, text, context);
   1623 
   1624                             populateGPlusOrHangoutsDataItemModel(itemModel);
   1625                             intent = itemModel.intent;
   1626                             alternateIntent = itemModel.alternateIntent;
   1627                             alternateContentDescription = itemModel.alternateContentDescription;
   1628                             header = itemModel.header;
   1629                             text = itemModel.text;
   1630                         } else {
   1631                             if (GPLUS_PROFILE_DATA_5_ADD_TO_CIRCLE.equals(
   1632                                     intent.getDataString())) {
   1633                                 icon = res.getDrawable(R.drawable.ic_add_to_circles_black_24);
   1634                             } else {
   1635                                 icon = res.getDrawable(R.drawable.ic_google_plus_24dp);
   1636                             }
   1637                         }
   1638                         break;
   1639                     case MIMETYPE_HANGOUTS:
   1640                         // If a secondDataItem is available, use it to build an entry with
   1641                         // alternate actions
   1642                         if (secondDataItem != null) {
   1643                             icon = res.getDrawable(R.drawable.ic_hangout_24dp);
   1644                             alternateIcon = res.getDrawable(R.drawable.ic_hangout_video_24dp);
   1645                             final GPlusOrHangoutsDataItemModel itemModel =
   1646                                     new GPlusOrHangoutsDataItemModel(intent, alternateIntent,
   1647                                             dataItem, secondDataItem, alternateContentDescription,
   1648                                             header, text, context);
   1649 
   1650                             populateGPlusOrHangoutsDataItemModel(itemModel);
   1651                             intent = itemModel.intent;
   1652                             alternateIntent = itemModel.alternateIntent;
   1653                             alternateContentDescription = itemModel.alternateContentDescription;
   1654                             header = itemModel.header;
   1655                             text = itemModel.text;
   1656                         } else {
   1657                             if (HANGOUTS_DATA_5_VIDEO.equals(intent.getDataString())) {
   1658                                 icon = res.getDrawable(R.drawable.ic_hangout_video_24dp);
   1659                             } else {
   1660                                 icon = res.getDrawable(R.drawable.ic_hangout_24dp);
   1661                             }
   1662                         }
   1663                         break;
   1664                     default:
   1665                         entryContextMenuInfo = new EntryContextMenuInfo(header, mimetype,
   1666                                 dataItem.getMimeType(), dataItem.getId(),
   1667                                 dataItem.isSuperPrimary());
   1668                         icon = ResolveCache.getInstance(context).getIcon(
   1669                                 dataItem.getMimeType(), intent);
   1670                         // Call mutate to create a new Drawable.ConstantState for color filtering
   1671                         if (icon != null) {
   1672                             icon.mutate();
   1673                         }
   1674                         shouldApplyColor = false;
   1675                 }
   1676             }
   1677         }
   1678 
   1679         if (intent != null) {
   1680             // Do not set the intent is there are no resolves
   1681             if (!PhoneCapabilityTester.isIntentRegistered(context, intent)) {
   1682                 intent = null;
   1683             }
   1684         }
   1685 
   1686         if (alternateIntent != null) {
   1687             // Do not set the alternate intent is there are no resolves
   1688             if (!PhoneCapabilityTester.isIntentRegistered(context, alternateIntent)) {
   1689                 alternateIntent = null;
   1690             } else if (TextUtils.isEmpty(alternateContentDescription)) {
   1691                 // Attempt to use package manager to find a suitable content description if needed
   1692                 alternateContentDescription.append(getIntentResolveLabel(alternateIntent, context));
   1693             }
   1694         }
   1695 
   1696         // If the Entry has no visual elements, return null
   1697         if (icon == null && TextUtils.isEmpty(header) && TextUtils.isEmpty(subHeader) &&
   1698                 subHeaderIcon == null && TextUtils.isEmpty(text) && textIcon == null) {
   1699             return null;
   1700         }
   1701 
   1702         // Ignore dataIds from the Me profile.
   1703         final int dataId = dataItem.getId() > Integer.MAX_VALUE ?
   1704                 -1 : (int) dataItem.getId();
   1705 
   1706         return new Entry(dataId, icon, header, subHeader, subHeaderIcon, text, textIcon,
   1707                 new SpannableString(primaryContentDescription.toString()),
   1708                 intent, alternateIcon, alternateIntent,
   1709                 alternateContentDescription.toString(), shouldApplyColor, isEditable,
   1710                 entryContextMenuInfo, thirdIcon, thirdIntent, thirdContentDescription,
   1711                 iconResourceId);
   1712     }
   1713 
   1714     private List<Entry> dataItemsToEntries(List<DataItem> dataItems,
   1715             MutableString aboutCardTitleOut) {
   1716         // Hangouts and G+ use two data items to create one entry.
   1717         if (dataItems.get(0).getMimeType().equals(MIMETYPE_GPLUS_PROFILE) ||
   1718                 dataItems.get(0).getMimeType().equals(MIMETYPE_HANGOUTS)) {
   1719             return gPlusOrHangoutsDataItemsToEntries(dataItems);
   1720         } else {
   1721             final List<Entry> entries = new ArrayList<>();
   1722             for (DataItem dataItem : dataItems) {
   1723                 final Entry entry = dataItemToEntry(dataItem, /* secondDataItem = */ null,
   1724                         this, mContactData, aboutCardTitleOut);
   1725                 if (entry != null) {
   1726                     entries.add(entry);
   1727                 }
   1728             }
   1729             return entries;
   1730         }
   1731     }
   1732 
   1733     /**
   1734      * G+ and Hangout entries are unique in that a single ExpandingEntryCardView.Entry consists
   1735      * of two data items. This method attempts to build each entry using the two data items if
   1736      * they are available. If there are more or less than two data items, a fall back is used
   1737      * and each data item gets its own entry.
   1738      */
   1739     private List<Entry> gPlusOrHangoutsDataItemsToEntries(List<DataItem> dataItems) {
   1740         final List<Entry> entries = new ArrayList<>();
   1741         final Map<Long, List<DataItem>> buckets = new HashMap<>();
   1742         // Put the data items into buckets based on the raw contact id
   1743         for (DataItem dataItem : dataItems) {
   1744             List<DataItem> bucket = buckets.get(dataItem.getRawContactId());
   1745             if (bucket == null) {
   1746                 bucket = new ArrayList<>();
   1747                 buckets.put(dataItem.getRawContactId(), bucket);
   1748             }
   1749             bucket.add(dataItem);
   1750         }
   1751 
   1752         // Use the buckets to build entries. If a bucket contains two data items, build the special
   1753         // entry, otherwise fall back to the normal entry.
   1754         for (List<DataItem> bucket : buckets.values()) {
   1755             if (bucket.size() == 2) {
   1756                 // Use the pair to build an entry
   1757                 final Entry entry = dataItemToEntry(bucket.get(0),
   1758                         /* secondDataItem = */ bucket.get(1), this, mContactData,
   1759                         /* aboutCardName = */ null);
   1760                 if (entry != null) {
   1761                     entries.add(entry);
   1762                 }
   1763             } else {
   1764                 for (DataItem dataItem : bucket) {
   1765                     final Entry entry = dataItemToEntry(dataItem, /* secondDataItem = */ null,
   1766                             this, mContactData, /* aboutCardName = */ null);
   1767                     if (entry != null) {
   1768                         entries.add(entry);
   1769                     }
   1770                 }
   1771             }
   1772         }
   1773         return entries;
   1774     }
   1775 
   1776     /**
   1777      * Used for statically passing around G+ or Hangouts data items and entry fields to
   1778      * populateGPlusOrHangoutsDataItemModel.
   1779      */
   1780     private static final class GPlusOrHangoutsDataItemModel {
   1781         public Intent intent;
   1782         public Intent alternateIntent;
   1783         public DataItem dataItem;
   1784         public DataItem secondDataItem;
   1785         public StringBuilder alternateContentDescription;
   1786         public String header;
   1787         public String text;
   1788         public Context context;
   1789 
   1790         public GPlusOrHangoutsDataItemModel(Intent intent, Intent alternateIntent, DataItem dataItem,
   1791                 DataItem secondDataItem, StringBuilder alternateContentDescription, String header,
   1792                 String text, Context context) {
   1793             this.intent = intent;
   1794             this.alternateIntent = alternateIntent;
   1795             this.dataItem = dataItem;
   1796             this.secondDataItem = secondDataItem;
   1797             this.alternateContentDescription = alternateContentDescription;
   1798             this.header = header;
   1799             this.text = text;
   1800             this.context = context;
   1801         }
   1802     }
   1803 
   1804     private static void populateGPlusOrHangoutsDataItemModel(
   1805             GPlusOrHangoutsDataItemModel dataModel) {
   1806         final Intent secondIntent = new Intent(Intent.ACTION_VIEW);
   1807         secondIntent.setDataAndType(ContentUris.withAppendedId(Data.CONTENT_URI,
   1808                 dataModel.secondDataItem.getId()), dataModel.secondDataItem.getMimeType());
   1809         // There is no guarantee the order the data items come in. Second
   1810         // data item does not necessarily mean it's the alternate.
   1811         // Hangouts video and Add to circles should be alternate. Swap if needed
   1812         if (HANGOUTS_DATA_5_VIDEO.equals(
   1813                 dataModel.dataItem.getContentValues().getAsString(Data.DATA5)) ||
   1814                 GPLUS_PROFILE_DATA_5_ADD_TO_CIRCLE.equals(
   1815                         dataModel.dataItem.getContentValues().getAsString(Data.DATA5))) {
   1816             dataModel.alternateIntent = dataModel.intent;
   1817             dataModel.alternateContentDescription = new StringBuilder(dataModel.header);
   1818 
   1819             dataModel.intent = secondIntent;
   1820             dataModel.header = dataModel.secondDataItem.buildDataStringForDisplay(dataModel.context,
   1821                     dataModel.secondDataItem.getDataKind());
   1822             dataModel.text = dataModel.secondDataItem.getDataKind().typeColumn;
   1823         } else if (HANGOUTS_DATA_5_MESSAGE.equals(
   1824                 dataModel.dataItem.getContentValues().getAsString(Data.DATA5)) ||
   1825                 GPLUS_PROFILE_DATA_5_VIEW_PROFILE.equals(
   1826                         dataModel.dataItem.getContentValues().getAsString(Data.DATA5))) {
   1827             dataModel.alternateIntent = secondIntent;
   1828             dataModel.alternateContentDescription = new StringBuilder(
   1829                     dataModel.secondDataItem.buildDataStringForDisplay(dataModel.context,
   1830                             dataModel.secondDataItem.getDataKind()));
   1831         }
   1832     }
   1833 
   1834     private static String getIntentResolveLabel(Intent intent, Context context) {
   1835         final List<ResolveInfo> matches = context.getPackageManager().queryIntentActivities(intent,
   1836                 PackageManager.MATCH_DEFAULT_ONLY);
   1837 
   1838         // Pick first match, otherwise best found
   1839         ResolveInfo bestResolve = null;
   1840         final int size = matches.size();
   1841         if (size == 1) {
   1842             bestResolve = matches.get(0);
   1843         } else if (size > 1) {
   1844             bestResolve = ResolveCache.getInstance(context).getBestResolve(intent, matches);
   1845         }
   1846 
   1847         if (bestResolve == null) {
   1848             return null;
   1849         }
   1850 
   1851         return String.valueOf(bestResolve.loadLabel(context.getPackageManager()));
   1852     }
   1853 
   1854     /**
   1855      * Asynchronously extract the most vibrant color from the PhotoView. Once extracted,
   1856      * apply this tint to {@link MultiShrinkScroller}. This operation takes about 20-30ms
   1857      * on a Nexus 5.
   1858      */
   1859     private void extractAndApplyTintFromPhotoViewAsynchronously() {
   1860         if (mScroller == null) {
   1861             return;
   1862         }
   1863         final Drawable imageViewDrawable = mPhotoView.getDrawable();
   1864         new AsyncTask<Void, Void, MaterialPalette>() {
   1865             @Override
   1866             protected MaterialPalette doInBackground(Void... params) {
   1867 
   1868                 if (imageViewDrawable instanceof BitmapDrawable && mContactData != null
   1869                         && mContactData.getThumbnailPhotoBinaryData() != null
   1870                         && mContactData.getThumbnailPhotoBinaryData().length > 0) {
   1871                     // Perform the color analysis on the thumbnail instead of the full sized
   1872                     // image, so that our results will be as similar as possible to the Bugle
   1873                     // app.
   1874                     final Bitmap bitmap = BitmapFactory.decodeByteArray(
   1875                             mContactData.getThumbnailPhotoBinaryData(), 0,
   1876                             mContactData.getThumbnailPhotoBinaryData().length);
   1877                     try {
   1878                         final int primaryColor = colorFromBitmap(bitmap);
   1879                         if (primaryColor != 0) {
   1880                             return mMaterialColorMapUtils.calculatePrimaryAndSecondaryColor(
   1881                                     primaryColor);
   1882                         }
   1883                     } finally {
   1884                         bitmap.recycle();
   1885                     }
   1886                 }
   1887                 if (imageViewDrawable instanceof LetterTileDrawable) {
   1888                     final int primaryColor = ((LetterTileDrawable) imageViewDrawable).getColor();
   1889                     return mMaterialColorMapUtils.calculatePrimaryAndSecondaryColor(primaryColor);
   1890                 }
   1891                 return MaterialColorMapUtils.getDefaultPrimaryAndSecondaryColors(getResources());
   1892             }
   1893 
   1894             @Override
   1895             protected void onPostExecute(MaterialPalette palette) {
   1896                 super.onPostExecute(palette);
   1897                 if (mHasComputedThemeColor) {
   1898                     // If we had previously computed a theme color from the contact photo,
   1899                     // then do not update the theme color. Changing the theme color several
   1900                     // seconds after QC has started, as a result of an updated/upgraded photo,
   1901                     // is a jarring experience. On the other hand, changing the theme color after
   1902                     // a rotation or onNewIntent() is perfectly fine.
   1903                     return;
   1904                 }
   1905                 // Check that the Photo has not changed. If it has changed, the new tint
   1906                 // color needs to be extracted
   1907                 if (imageViewDrawable == mPhotoView.getDrawable()) {
   1908                     mHasComputedThemeColor = true;
   1909                     setThemeColor(palette);
   1910                 }
   1911             }
   1912         }.execute();
   1913     }
   1914 
   1915     private void setThemeColor(MaterialPalette palette) {
   1916         // If the color is invalid, use the predefined default
   1917         mColorFilterColor = palette.mPrimaryColor;
   1918         mScroller.setHeaderTintColor(mColorFilterColor);
   1919         mStatusBarColor = palette.mSecondaryColor;
   1920         updateStatusBarColor();
   1921 
   1922         mColorFilter =
   1923                 new PorterDuffColorFilter(mColorFilterColor, PorterDuff.Mode.SRC_ATOP);
   1924         mContactCard.setColorAndFilter(mColorFilterColor, mColorFilter);
   1925         mRecentCard.setColorAndFilter(mColorFilterColor, mColorFilter);
   1926         mAboutCard.setColorAndFilter(mColorFilterColor, mColorFilter);
   1927     }
   1928 
   1929     private void updateStatusBarColor() {
   1930         if (mScroller == null) {
   1931             return;
   1932         }
   1933         final int desiredStatusBarColor;
   1934         // Only use a custom status bar color if QuickContacts touches the top of the viewport.
   1935         if (mScroller.getScrollNeededToBeFullScreen() <= 0) {
   1936             desiredStatusBarColor = mStatusBarColor;
   1937         } else {
   1938             desiredStatusBarColor = Color.TRANSPARENT;
   1939         }
   1940         // Animate to the new color.
   1941         final ObjectAnimator animation = ObjectAnimator.ofInt(getWindow(), "statusBarColor",
   1942                 getWindow().getStatusBarColor(), desiredStatusBarColor);
   1943         animation.setDuration(ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION);
   1944         animation.setEvaluator(new ArgbEvaluator());
   1945         animation.start();
   1946     }
   1947 
   1948     private int colorFromBitmap(Bitmap bitmap) {
   1949         // Author of Palette recommends using 24 colors when analyzing profile photos.
   1950         final int NUMBER_OF_PALETTE_COLORS = 24;
   1951         final Palette palette = Palette.generate(bitmap, NUMBER_OF_PALETTE_COLORS);
   1952         if (palette != null && palette.getVibrantSwatch() != null) {
   1953             return palette.getVibrantSwatch().getRgb();
   1954         }
   1955         return 0;
   1956     }
   1957 
   1958     private List<Entry> contactInteractionsToEntries(List<ContactInteraction> interactions) {
   1959         final List<Entry> entries = new ArrayList<>();
   1960         for (ContactInteraction interaction : interactions) {
   1961             if (interaction == null) {
   1962                 continue;
   1963             }
   1964             entries.add(new Entry(/* id = */ -1,
   1965                     interaction.getIcon(this),
   1966                     interaction.getViewHeader(this),
   1967                     interaction.getViewBody(this),
   1968                     interaction.getBodyIcon(this),
   1969                     interaction.getViewFooter(this),
   1970                     interaction.getFooterIcon(this),
   1971                     interaction.getContentDescription(this),
   1972                     interaction.getIntent(),
   1973                     /* alternateIcon = */ null,
   1974                     /* alternateIntent = */ null,
   1975                     /* alternateContentDescription = */ null,
   1976                     /* shouldApplyColor = */ true,
   1977                     /* isEditable = */ false,
   1978                     /* EntryContextMenuInfo = */ null,
   1979                     /* thirdIcon = */ null,
   1980                     /* thirdIntent = */ null,
   1981                     /* thirdContentDescription = */ null,
   1982                     interaction.getIconResourceId()));
   1983         }
   1984         return entries;
   1985     }
   1986 
   1987     private final LoaderCallbacks<Contact> mLoaderContactCallbacks =
   1988             new LoaderCallbacks<Contact>() {
   1989         @Override
   1990         public void onLoaderReset(Loader<Contact> loader) {
   1991             mContactData = null;
   1992         }
   1993 
   1994         @Override
   1995         public void onLoadFinished(Loader<Contact> loader, Contact data) {
   1996             Trace.beginSection("onLoadFinished()");
   1997             try {
   1998 
   1999                 if (isFinishing()) {
   2000                     return;
   2001                 }
   2002                 if (data.isError()) {
   2003                     // This means either the contact is invalid or we had an
   2004                     // internal error such as an acore crash.
   2005                     Log.i(TAG, "Failed to load contact: " + ((ContactLoader)loader).getLookupUri());
   2006                     Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage,
   2007                             Toast.LENGTH_LONG).show();
   2008                     finish();
   2009                     return;
   2010                 }
   2011                 if (data.isNotFound()) {
   2012                     Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri());
   2013                     Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage,
   2014                             Toast.LENGTH_LONG).show();
   2015                     finish();
   2016                     return;
   2017                 }
   2018 
   2019                 bindContactData(data);
   2020 
   2021             } finally {
   2022                 Trace.endSection();
   2023             }
   2024         }
   2025 
   2026         @Override
   2027         public Loader<Contact> onCreateLoader(int id, Bundle args) {
   2028             if (mLookupUri == null) {
   2029                 Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early");
   2030             }
   2031             // Load all contact data. We need loadGroupMetaData=true to determine whether the
   2032             // contact is invisible. If it is, we need to display an "Add to Contacts" MenuItem.
   2033             return new ContactLoader(getApplicationContext(), mLookupUri,
   2034                     true /*loadGroupMetaData*/, false /*loadInvitableAccountTypes*/,
   2035                     true /*postViewNotification*/, true /*computeFormattedPhoneNumber*/);
   2036         }
   2037     };
   2038 
   2039     @Override
   2040     public void onBackPressed() {
   2041         if (mScroller != null) {
   2042             if (!mIsExitAnimationInProgress) {
   2043                 mScroller.scrollOffBottom();
   2044             }
   2045         } else {
   2046             super.onBackPressed();
   2047         }
   2048     }
   2049 
   2050     @Override
   2051     public void finish() {
   2052         super.finish();
   2053 
   2054         // override transitions to skip the standard window animations
   2055         overridePendingTransition(0, 0);
   2056     }
   2057 
   2058     private final LoaderCallbacks<List<ContactInteraction>> mLoaderInteractionsCallbacks =
   2059             new LoaderCallbacks<List<ContactInteraction>>() {
   2060 
   2061         @Override
   2062         public Loader<List<ContactInteraction>> onCreateLoader(int id, Bundle args) {
   2063             Loader<List<ContactInteraction>> loader = null;
   2064             switch (id) {
   2065                 case LOADER_SMS_ID:
   2066                     loader = new SmsInteractionsLoader(
   2067                             QuickContactActivity.this,
   2068                             args.getStringArray(KEY_LOADER_EXTRA_PHONES),
   2069                             MAX_SMS_RETRIEVE);
   2070                     break;
   2071                 case LOADER_CALENDAR_ID:
   2072                     final String[] emailsArray = args.getStringArray(KEY_LOADER_EXTRA_EMAILS);
   2073                     List<String> emailsList = null;
   2074                     if (emailsArray != null) {
   2075                         emailsList = Arrays.asList(args.getStringArray(KEY_LOADER_EXTRA_EMAILS));
   2076                     }
   2077                     loader = new CalendarInteractionsLoader(
   2078                             QuickContactActivity.this,
   2079                             emailsList,
   2080                             MAX_FUTURE_CALENDAR_RETRIEVE,
   2081                             MAX_PAST_CALENDAR_RETRIEVE,
   2082                             FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR,
   2083                             PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR);
   2084                     break;
   2085                 case LOADER_CALL_LOG_ID:
   2086                     loader = new CallLogInteractionsLoader(
   2087                             QuickContactActivity.this,
   2088                             args.getStringArray(KEY_LOADER_EXTRA_PHONES),
   2089                             MAX_CALL_LOG_RETRIEVE);
   2090             }
   2091             return loader;
   2092         }
   2093 
   2094         @Override
   2095         public void onLoadFinished(Loader<List<ContactInteraction>> loader,
   2096                 List<ContactInteraction> data) {
   2097             mRecentLoaderResults.put(loader.getId(), data);
   2098 
   2099             if (isAllRecentDataLoaded()) {
   2100                 bindRecentData();
   2101             }
   2102         }
   2103 
   2104         @Override
   2105         public void onLoaderReset(Loader<List<ContactInteraction>> loader) {
   2106             mRecentLoaderResults.remove(loader.getId());
   2107         }
   2108     };
   2109 
   2110     private boolean isAllRecentDataLoaded() {
   2111         return mRecentLoaderResults.size() == mRecentLoaderIds.length;
   2112     }
   2113 
   2114     private void bindRecentData() {
   2115         final List<ContactInteraction> allInteractions = new ArrayList<>();
   2116         final List<List<Entry>> interactionsWrapper = new ArrayList<>();
   2117 
   2118         // Serialize mRecentLoaderResults into a single list. This should be done on the main
   2119         // thread to avoid races against mRecentLoaderResults edits.
   2120         for (List<ContactInteraction> loaderInteractions : mRecentLoaderResults.values()) {
   2121             allInteractions.addAll(loaderInteractions);
   2122         }
   2123 
   2124         mRecentDataTask = new AsyncTask<Void, Void, Void>() {
   2125             @Override
   2126             protected Void doInBackground(Void... params) {
   2127                 Trace.beginSection("sort recent loader results");
   2128 
   2129                 // Sort the interactions by most recent
   2130                 Collections.sort(allInteractions, new Comparator<ContactInteraction>() {
   2131                     @Override
   2132                     public int compare(ContactInteraction a, ContactInteraction b) {
   2133                         if (a == null && b == null) {
   2134                             return 0;
   2135                         }
   2136                         if (a == null) {
   2137                             return 1;
   2138                         }
   2139                         if (b == null) {
   2140                             return -1;
   2141                         }
   2142                         if (a.getInteractionDate() > b.getInteractionDate()) {
   2143                             return -1;
   2144                         }
   2145                         if (a.getInteractionDate() == b.getInteractionDate()) {
   2146                             return 0;
   2147                         }
   2148                         return 1;
   2149                     }
   2150                 });
   2151 
   2152                 Trace.endSection();
   2153                 Trace.beginSection("contactInteractionsToEntries");
   2154 
   2155                 // Wrap each interaction in its own list so that an icon is displayed for each entry
   2156                 for (Entry contactInteraction : contactInteractionsToEntries(allInteractions)) {
   2157                     List<Entry> entryListWrapper = new ArrayList<>(1);
   2158                     entryListWrapper.add(contactInteraction);
   2159                     interactionsWrapper.add(entryListWrapper);
   2160                 }
   2161 
   2162                 Trace.endSection();
   2163                 return null;
   2164             }
   2165 
   2166             @Override
   2167             protected void onPostExecute(Void aVoid) {
   2168                 super.onPostExecute(aVoid);
   2169                 Trace.beginSection("initialize recents card");
   2170 
   2171                 if (allInteractions.size() > 0) {
   2172                     mRecentCard.initialize(interactionsWrapper,
   2173                     /* numInitialVisibleEntries = */ MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN,
   2174                     /* isExpanded = */ mRecentCard.isExpanded(), /* isAlwaysExpanded = */ false,
   2175                             mExpandingEntryCardViewListener, mScroller);
   2176                     mRecentCard.setVisibility(View.VISIBLE);
   2177                 }
   2178 
   2179                 Trace.endSection();
   2180 
   2181                 // About card is initialized along with the contact card, but since it appears after
   2182                 // the recent card in the UI, we hold off until making it visible until the recent
   2183                 // card is also ready to avoid stuttering.
   2184                 if (mAboutCard.shouldShow()) {
   2185                     mAboutCard.setVisibility(View.VISIBLE);
   2186                 } else {
   2187                     mAboutCard.setVisibility(View.GONE);
   2188                 }
   2189                 mRecentDataTask = null;
   2190             }
   2191         };
   2192         mRecentDataTask.execute();
   2193     }
   2194 
   2195     @Override
   2196     protected void onStop() {
   2197         super.onStop();
   2198 
   2199         if (mEntriesAndActionsTask != null) {
   2200             // Once the activity is stopped, we will no longer want to bind mEntriesAndActionsTask's
   2201             // results on the UI thread. In some circumstances Activities are killed without
   2202             // onStop() being called. This is not a problem, because in these circumstances
   2203             // the entire process will be killed.
   2204             mEntriesAndActionsTask.cancel(/* mayInterruptIfRunning = */ false);
   2205         }
   2206         if (mRecentDataTask != null) {
   2207             mRecentDataTask.cancel(/* mayInterruptIfRunning = */ false);
   2208         }
   2209     }
   2210 
   2211     /**
   2212      * Returns true if it is possible to edit the current contact.
   2213      */
   2214     private boolean isContactEditable() {
   2215         return mContactData != null && !mContactData.isDirectoryEntry();
   2216     }
   2217 
   2218     /**
   2219      * Returns true if it is possible to share the current contact.
   2220      */
   2221     private boolean isContactShareable() {
   2222         return mContactData != null && !mContactData.isDirectoryEntry();
   2223     }
   2224 
   2225     private Intent getEditContactIntent() {
   2226         return EditorIntents.createCompactEditContactIntent(
   2227                 mContactData.getLookupUri(),
   2228                 mHasComputedThemeColor
   2229                         ? new MaterialPalette(mColorFilterColor, mStatusBarColor) : null,
   2230                 /* updatedPhotos =*/ null,
   2231                 mContactData.getPhotoId(),
   2232                 mContactData.getNameRawContactId());
   2233     }
   2234 
   2235     private void editContact() {
   2236         mHasIntentLaunched = true;
   2237         mContactLoader.cacheResult();
   2238         startActivityForResult(getEditContactIntent(), REQUEST_CODE_CONTACT_EDITOR_ACTIVITY);
   2239     }
   2240 
   2241     private void deleteContact() {
   2242         final Uri contactUri = mContactData.getLookupUri();
   2243         ContactDeletionInteraction.start(this, contactUri, /* finishActivityWhenDone =*/ true);
   2244     }
   2245 
   2246     private void toggleStar(MenuItem starredMenuItem) {
   2247         // Make sure there is a contact
   2248         if (mContactData != null) {
   2249             // Read the current starred value from the UI instead of using the last
   2250             // loaded state. This allows rapid tapping without writing the same
   2251             // value several times
   2252             final boolean isStarred = starredMenuItem.isChecked();
   2253 
   2254             // To improve responsiveness, swap out the picture (and tag) in the UI already
   2255             ContactDisplayUtils.configureStarredMenuItem(starredMenuItem,
   2256                     mContactData.isDirectoryEntry(), mContactData.isUserProfile(),
   2257                     !isStarred);
   2258 
   2259             // Now perform the real save
   2260             final Intent intent = ContactSaveService.createSetStarredIntent(
   2261                     QuickContactActivity.this, mContactData.getLookupUri(), !isStarred);
   2262             startService(intent);
   2263 
   2264             final CharSequence accessibilityText = !isStarred
   2265                     ? getResources().getText(R.string.description_action_menu_add_star)
   2266                     : getResources().getText(R.string.description_action_menu_remove_star);
   2267             // Accessibility actions need to have an associated view. We can't access the MenuItem's
   2268             // underlying view, so put this accessibility action on the root view.
   2269             mScroller.announceForAccessibility(accessibilityText);
   2270         }
   2271     }
   2272 
   2273     private void shareContact() {
   2274         final String lookupKey = mContactData.getLookupKey();
   2275         final Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey);
   2276         final Intent intent = new Intent(Intent.ACTION_SEND);
   2277         intent.setType(Contacts.CONTENT_VCARD_TYPE);
   2278         intent.putExtra(Intent.EXTRA_STREAM, shareUri);
   2279 
   2280         // Launch chooser to share contact via
   2281         final CharSequence chooseTitle = getText(R.string.share_via);
   2282         final Intent chooseIntent = Intent.createChooser(intent, chooseTitle);
   2283 
   2284         try {
   2285             mHasIntentLaunched = true;
   2286             ImplicitIntentsUtil.startActivityOutsideApp(this, chooseIntent);
   2287         } catch (final ActivityNotFoundException ex) {
   2288             Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show();
   2289         }
   2290     }
   2291 
   2292     /**
   2293      * Creates a launcher shortcut with the current contact.
   2294      */
   2295     private void createLauncherShortcutWithContact() {
   2296         final ShortcutIntentBuilder builder = new ShortcutIntentBuilder(this,
   2297                 new OnShortcutIntentCreatedListener() {
   2298 
   2299                     @Override
   2300                     public void onShortcutIntentCreated(Uri uri, Intent shortcutIntent) {
   2301                         // Broadcast the shortcutIntent to the launcher to create a
   2302                         // shortcut to this contact
   2303                         shortcutIntent.setAction(ACTION_INSTALL_SHORTCUT);
   2304                         QuickContactActivity.this.sendBroadcast(shortcutIntent);
   2305 
   2306                         // Send a toast to give feedback to the user that a shortcut to this
   2307                         // contact was added to the launcher.
   2308                         Toast.makeText(QuickContactActivity.this,
   2309                                 R.string.createContactShortcutSuccessful,
   2310                                 Toast.LENGTH_SHORT).show();
   2311                     }
   2312 
   2313                 });
   2314         builder.createContactShortcutIntent(mContactData.getLookupUri());
   2315     }
   2316 
   2317     private boolean isShortcutCreatable() {
   2318         if (mContactData == null || mContactData.isUserProfile() ||
   2319                 mContactData.isDirectoryEntry()) {
   2320             return false;
   2321         }
   2322         final Intent createShortcutIntent = new Intent();
   2323         createShortcutIntent.setAction(ACTION_INSTALL_SHORTCUT);
   2324         final List<ResolveInfo> receivers = getPackageManager()
   2325                 .queryBroadcastReceivers(createShortcutIntent, 0);
   2326         return receivers != null && receivers.size() > 0;
   2327     }
   2328 
   2329     @Override
   2330     public boolean onCreateOptionsMenu(Menu menu) {
   2331         final MenuInflater inflater = getMenuInflater();
   2332         inflater.inflate(R.menu.quickcontact, menu);
   2333         return true;
   2334     }
   2335 
   2336     @Override
   2337     public boolean onPrepareOptionsMenu(Menu menu) {
   2338         if (mContactData != null) {
   2339             final MenuItem starredMenuItem = menu.findItem(R.id.menu_star);
   2340             ContactDisplayUtils.configureStarredMenuItem(starredMenuItem,
   2341                     mContactData.isDirectoryEntry(), mContactData.isUserProfile(),
   2342                     mContactData.getStarred());
   2343 
   2344             // Configure edit MenuItem
   2345             final MenuItem editMenuItem = menu.findItem(R.id.menu_edit);
   2346             editMenuItem.setVisible(true);
   2347             if (DirectoryContactUtil.isDirectoryContact(mContactData) || InvisibleContactUtil
   2348                     .isInvisibleAndAddable(mContactData, this)) {
   2349                 editMenuItem.setIcon(R.drawable.ic_person_add_tinted_24dp);
   2350                 editMenuItem.setTitle(R.string.menu_add_contact);
   2351             } else if (isContactEditable()) {
   2352                 editMenuItem.setIcon(R.drawable.ic_create_24dp);
   2353                 editMenuItem.setTitle(R.string.menu_editContact);
   2354             } else {
   2355                 editMenuItem.setVisible(false);
   2356             }
   2357 
   2358             final MenuItem deleteMenuItem = menu.findItem(R.id.menu_delete);
   2359             deleteMenuItem.setVisible(isContactEditable());
   2360 
   2361             final MenuItem shareMenuItem = menu.findItem(R.id.menu_share);
   2362             shareMenuItem.setVisible(isContactShareable());
   2363 
   2364             final MenuItem shortcutMenuItem = menu.findItem(R.id.menu_create_contact_shortcut);
   2365             shortcutMenuItem.setVisible(isShortcutCreatable());
   2366 
   2367             final MenuItem helpMenu = menu.findItem(R.id.menu_help);
   2368             helpMenu.setVisible(HelpUtils.isHelpAndFeedbackAvailable());
   2369 
   2370             return true;
   2371         }
   2372         return false;
   2373     }
   2374 
   2375     @Override
   2376     public boolean onOptionsItemSelected(MenuItem item) {
   2377         switch (item.getItemId()) {
   2378             case R.id.menu_star:
   2379                 toggleStar(item);
   2380                 return true;
   2381             case R.id.menu_edit:
   2382                 if (DirectoryContactUtil.isDirectoryContact(mContactData)) {
   2383                     // This action is used to launch the contact selector, with the option of
   2384                     // creating a new contact. Creating a new contact is an INSERT, while selecting
   2385                     // an exisiting one is an edit. The fields in the edit screen will be
   2386                     // prepopulated with data.
   2387 
   2388                     final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
   2389                     intent.setType(Contacts.CONTENT_ITEM_TYPE);
   2390 
   2391                     ArrayList<ContentValues> values = mContactData.getContentValues();
   2392 
   2393                     // Only pre-fill the name field if the provided display name is an nickname
   2394                     // or better (e.g. structured name, nickname)
   2395                     if (mContactData.getDisplayNameSource() >= DisplayNameSources.NICKNAME) {
   2396                         intent.putExtra(Intents.Insert.NAME, mContactData.getDisplayName());
   2397                     } else if (mContactData.getDisplayNameSource()
   2398                             == DisplayNameSources.ORGANIZATION) {
   2399                         // This is probably an organization. Instead of copying the organization
   2400                         // name into a name entry, copy it into the organization entry. This
   2401                         // way we will still consider the contact an organization.
   2402                         final ContentValues organization = new ContentValues();
   2403                         organization.put(Organization.COMPANY, mContactData.getDisplayName());
   2404                         organization.put(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
   2405                         values.add(organization);
   2406                     }
   2407 
   2408                     // Last time used and times used are aggregated values from the usage stat
   2409                     // table. They need to be removed from data values so the SQL table can insert
   2410                     // properly
   2411                     for (ContentValues value : values) {
   2412                         value.remove(Data.LAST_TIME_USED);
   2413                         value.remove(Data.TIMES_USED);
   2414                     }
   2415                     intent.putExtra(Intents.Insert.DATA, values);
   2416 
   2417                     // If the contact can only export to the same account, add it to the intent.
   2418                     // Otherwise the ContactEditorFragment will show a dialog for selecting an
   2419                     // account.
   2420                     if (mContactData.getDirectoryExportSupport() ==
   2421                             Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY) {
   2422                         intent.putExtra(Intents.Insert.EXTRA_ACCOUNT,
   2423                                 new Account(mContactData.getDirectoryAccountName(),
   2424                                         mContactData.getDirectoryAccountType()));
   2425                         intent.putExtra(Intents.Insert.EXTRA_DATA_SET,
   2426                                 mContactData.getRawContacts().get(0).getDataSet());
   2427                     }
   2428 
   2429                     // Add this flag to disable the delete menu option on directory contact joins
   2430                     // with local contacts. The delete option is ambiguous when joining contacts.
   2431                     intent.putExtra(ContactEditorFragment.INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION,
   2432                             true);
   2433 
   2434                     startActivityForResult(intent, REQUEST_CODE_CONTACT_SELECTION_ACTIVITY);
   2435                 } else if (InvisibleContactUtil.isInvisibleAndAddable(mContactData, this)) {
   2436                     InvisibleContactUtil.addToDefaultGroup(mContactData, this);
   2437                 } else if (isContactEditable()) {
   2438                     editContact();
   2439                 }
   2440                 return true;
   2441             case R.id.menu_delete:
   2442                 if (isContactEditable()) {
   2443                     deleteContact();
   2444                 }
   2445                 return true;
   2446             case R.id.menu_share:
   2447                 if (isContactShareable()) {
   2448                     shareContact();
   2449                 }
   2450                 return true;
   2451             case R.id.menu_create_contact_shortcut:
   2452                 if (isShortcutCreatable()) {
   2453                     createLauncherShortcutWithContact();
   2454                 }
   2455                 return true;
   2456             case R.id.menu_help:
   2457                 HelpUtils.launchHelpAndFeedbackForContactScreen(this);
   2458                 return true;
   2459             default:
   2460                 return super.onOptionsItemSelected(item);
   2461         }
   2462     }
   2463 }
   2464