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