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