• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
onAggregationSuggestionChange()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 
collapseSuggestionList()557     private void collapseSuggestionList() {
558         mCollapsedSuggestionCardView.setVisibility(View.VISIBLE);
559         mExpandSuggestionCardView.setVisibility(View.GONE);
560         mIsSuggestionListCollapsed = true;
561     }
562 
expandSuggestionList()563     private void expandSuggestionList() {
564         mCollapsedSuggestionCardView.setVisibility(View.GONE);
565         mExpandSuggestionCardView.setVisibility(View.VISIBLE);
566         mIsSuggestionListCollapsed = false;
567     }
568 
inflateSuggestionListView(final Suggestion suggestion)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 
enableLinkButton()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
onContactsJoined()662     public void onContactsJoined() {
663         disableLinkButton();
664     }
665 
disableLinkButton()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
onContextItemSelected(MenuItem item)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 
SelectAccountDialogFragmentListener()760         public SelectAccountDialogFragmentListener() {}
761 
762         @Override
onAccountChosen(AccountWithDataSet account, Bundle extraArgs)763         public void onAccountChosen(AccountWithDataSet account, Bundle extraArgs) {
764             DirectoryContactUtil.createCopy(mQuickContactActivity.mContactData.getContentValues(),
765                     account, mQuickContactActivity);
766         }
767 
768         @Override
onAccountSelectorCancelled()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          */
setQuickContactActivity(QuickContactActivity quickContactActivity)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
dispatchTouchEvent(MotionEvent ev)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
onCreate(Bundle savedInstanceState)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
onActivityResult(int requestCode, int resultCode, Intent data)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
onNewIntent(Intent intent)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
onSaveInstanceState(Bundle savedInstanceState)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 
processIntent(Intent intent)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 
destroyInteractionLoaders()1189     private void destroyInteractionLoaders() {
1190         for (int interactionLoaderId : mRecentLoaderIds) {
1191             getLoaderManager().destroyLoader(interactionLoaderId);
1192         }
1193     }
1194 
runEntranceAnimation()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 
isMultiWindowOnPhone()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. */
setHeaderNameText(int resId)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. */
setHeaderNameText(String value, boolean isPhoneNumber)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      */
isMimeExcluded(String mimeType)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      */
bindContactData(final Contact data)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 
bindDataToCards(Cp2DataCardModel cp2DataCardModel)1291     private void bindDataToCards(Cp2DataCardModel cp2DataCardModel) {
1292         startInteractionLoaders(cp2DataCardModel);
1293         populateContactAndAboutCard(cp2DataCardModel, /* shouldAddPhoneticName */ true);
1294         populateSuggestionCard();
1295     }
1296 
startInteractionLoaders(Cp2DataCardModel cp2DataCardModel)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 
showActivity()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 
buildAboutCardEntries(Map<String, List<DataItem>> dataItemsMap)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
onResume()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 
populateSuggestionCard()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 
populateContactAndAboutCard(Cp2DataCardModel cp2DataCardModel, boolean shouldAddPhoneticName)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      */
initializeNoContactDetailCard()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      */
generateDataModelFromContact( Contact data)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      */
dataItemToEntry(DataItem dataItem, DataItem secondDataItem, Context context, Contact contactData, final MutableString aboutCardName)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 
dataItemsToEntries(List<DataItem> dataItems, MutableString aboutCardTitleOut)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      */
gPlusOrHangoutsDataItemsToEntries(List<DataItem> dataItems)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 
GPlusOrHangoutsDataItemModel(Intent intent, Intent alternateIntent, DataItem dataItem, DataItem secondDataItem, StringBuilder alternateContentDescription, String header, String text, Context context)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 
populateGPlusOrHangoutsDataItemModel( GPlusOrHangoutsDataItemModel dataModel)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 
getIntentResolveLabel(Intent intent, Context context)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      */
extractAndApplyTintFromPhotoViewAsynchronously()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 
setThemeColor(MaterialPalette palette)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 
updateStatusBarColor()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 
colorFromBitmap(Bitmap bitmap)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 
contactInteractionsToEntries(List<ContactInteraction> interactions)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
onBackPressed()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
finish()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 
isAllRecentDataLoaded()2531     private boolean isAllRecentDataLoaded() {
2532         return mRecentLoaderResults.size() == mRecentLoaderIds.length;
2533     }
2534 
bindRecentData()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
onStop()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
onDestroy()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      */
isContactEditable()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      */
isContactShareable()2650     private boolean isContactShareable() {
2651         return mContactData != null && !mContactData.isDirectoryEntry();
2652     }
2653 
getEditContactIntent()2654     private Intent getEditContactIntent() {
2655         return EditorIntents.createCompactEditContactIntent(
2656                 mContactData.getLookupUri(),
2657                 mHasComputedThemeColor
2658                         ? new MaterialPalette(mColorFilterColor, mStatusBarColor) : null,
2659                 mContactData.getPhotoId());
2660     }
2661 
editContact()2662     private void editContact() {
2663         mHasIntentLaunched = true;
2664         mContactLoader.cacheResult();
2665         startActivityForResult(getEditContactIntent(), REQUEST_CODE_CONTACT_EDITOR_ACTIVITY);
2666     }
2667 
deleteContact()2668     private void deleteContact() {
2669         final Uri contactUri = mContactData.getLookupUri();
2670         ContactDeletionInteraction.start(this, contactUri, /* finishActivityWhenDone =*/ true);
2671     }
2672 
toggleStar(MenuItem starredMenuItem)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 
shareContact()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      */
createLauncherShortcutWithContact()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 
isShortcutCreatable()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
onCreateOptionsMenu(Menu menu)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
onPrepareOptionsMenu(Menu menu)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
onOptionsItemSelected(MenuItem item)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