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