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