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