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