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