QuickContactActivity.java revision faf973989af7ac3b735c2aee1bec22ef6608b123
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.animation.Animator;
20import android.animation.Animator.AnimatorListener;
21import android.animation.AnimatorListenerAdapter;
22import android.animation.ArgbEvaluator;
23import android.animation.ObjectAnimator;
24import android.app.Activity;
25import android.app.Fragment;
26import android.app.LoaderManager.LoaderCallbacks;
27import android.app.SearchManager;
28import android.content.ActivityNotFoundException;
29import android.content.ComponentName;
30import android.content.ContentUris;
31import android.content.ContentValues;
32import android.content.Intent;
33import android.content.Loader;
34import android.graphics.Bitmap;
35import android.graphics.Color;
36import android.graphics.PorterDuff;
37import android.graphics.PorterDuffColorFilter;
38import android.graphics.drawable.BitmapDrawable;
39import android.graphics.drawable.ColorDrawable;
40import android.graphics.drawable.Drawable;
41import android.net.ParseException;
42import android.net.Uri;
43import android.net.WebAddress;
44import android.os.AsyncTask;
45import android.os.Bundle;
46import android.os.Trace;
47import android.provider.CalendarContract;
48import android.provider.ContactsContract;
49import android.provider.ContactsContract.CommonDataKinds.Email;
50import android.provider.ContactsContract.CommonDataKinds.Event;
51import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
52import android.provider.ContactsContract.CommonDataKinds.Identity;
53import android.provider.ContactsContract.CommonDataKinds.Im;
54import android.provider.ContactsContract.CommonDataKinds.Nickname;
55import android.provider.ContactsContract.CommonDataKinds.Note;
56import android.provider.ContactsContract.CommonDataKinds.Organization;
57import android.provider.ContactsContract.CommonDataKinds.Phone;
58import android.provider.ContactsContract.CommonDataKinds.Relation;
59import android.provider.ContactsContract.CommonDataKinds.SipAddress;
60import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
61import android.provider.ContactsContract.CommonDataKinds.Website;
62import android.provider.ContactsContract.Contacts;
63import android.provider.ContactsContract.DisplayNameSources;
64import android.provider.ContactsContract.DataUsageFeedback;
65import android.provider.ContactsContract.QuickContact;
66import android.provider.ContactsContract.RawContacts;
67import android.support.v7.graphics.Palette;
68import android.text.TextUtils;
69import android.util.Log;
70import android.util.Pair;
71import android.view.Menu;
72import android.view.MenuInflater;
73import android.view.MenuItem;
74import android.view.View;
75import android.view.View.OnClickListener;
76import android.view.WindowManager;
77import android.widget.ImageView;
78import android.widget.Toast;
79import android.widget.Toolbar;
80
81import com.android.contacts.ContactSaveService;
82import com.android.contacts.ContactsActivity;
83import com.android.contacts.NfcHandler;
84import com.android.contacts.R;
85import com.android.contacts.common.CallUtil;
86import com.android.contacts.common.Collapser;
87import com.android.contacts.common.ContactsUtils;
88import com.android.contacts.common.editor.SelectAccountDialogFragment;
89import com.android.contacts.common.lettertiles.LetterTileDrawable;
90import com.android.contacts.common.list.ShortcutIntentBuilder;
91import com.android.contacts.common.list.ShortcutIntentBuilder.OnShortcutIntentCreatedListener;
92import com.android.contacts.common.model.AccountTypeManager;
93import com.android.contacts.common.model.Contact;
94import com.android.contacts.common.model.ContactLoader;
95import com.android.contacts.common.model.RawContact;
96import com.android.contacts.common.model.account.AccountType;
97import com.android.contacts.common.model.account.AccountWithDataSet;
98import com.android.contacts.common.model.dataitem.DataItem;
99import com.android.contacts.common.model.dataitem.DataKind;
100import com.android.contacts.common.model.dataitem.EmailDataItem;
101import com.android.contacts.common.model.dataitem.EventDataItem;
102import com.android.contacts.common.model.dataitem.ImDataItem;
103import com.android.contacts.common.model.dataitem.NicknameDataItem;
104import com.android.contacts.common.model.dataitem.NoteDataItem;
105import com.android.contacts.common.model.dataitem.OrganizationDataItem;
106import com.android.contacts.common.model.dataitem.PhoneDataItem;
107import com.android.contacts.common.model.dataitem.RelationDataItem;
108import com.android.contacts.common.model.dataitem.SipAddressDataItem;
109import com.android.contacts.common.model.dataitem.StructuredNameDataItem;
110import com.android.contacts.common.model.dataitem.StructuredPostalDataItem;
111import com.android.contacts.common.model.dataitem.WebsiteDataItem;
112import com.android.contacts.common.util.DateUtils;
113import com.android.contacts.common.util.MaterialColorMapUtils;
114import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette;
115import com.android.contacts.detail.ContactDetailDisplayUtils;
116import com.android.contacts.interactions.CalendarInteractionsLoader;
117import com.android.contacts.interactions.CallLogInteractionsLoader;
118import com.android.contacts.interactions.ContactDeletionInteraction;
119import com.android.contacts.interactions.ContactInteraction;
120import com.android.contacts.interactions.SmsInteractionsLoader;
121import com.android.contacts.quickcontact.ExpandingEntryCardView.Entry;
122import com.android.contacts.quickcontact.ExpandingEntryCardView.ExpandingEntryCardViewListener;
123import com.android.contacts.util.ImageViewDrawableSetter;
124import com.android.contacts.util.PhoneCapabilityTester;
125import com.android.contacts.util.SchedulingUtils;
126import com.android.contacts.util.StructuredPostalUtils;
127import com.android.contacts.widget.MultiShrinkScroller;
128import com.android.contacts.widget.MultiShrinkScroller.MultiShrinkScrollerListener;
129
130import com.google.common.base.Preconditions;
131import com.google.common.collect.Lists;
132
133import java.util.ArrayList;
134import java.util.Arrays;
135import java.util.Calendar;
136import java.util.Collections;
137import java.util.Comparator;
138import java.util.Date;
139import java.util.HashMap;
140import java.util.List;
141import java.util.Map;
142
143/**
144 * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads
145 * data asynchronously, and then shows a popup with details centered around
146 * {@link Intent#getSourceBounds()}.
147 */
148public class QuickContactActivity extends ContactsActivity {
149
150    /**
151     * QuickContacts immediately takes up the full screen. All possible information is shown.
152     * This value for {@link android.provider.ContactsContract.QuickContact#EXTRA_MODE}
153     * should only be used by the Contacts app.
154     */
155    public static final int MODE_FULLY_EXPANDED = 4;
156
157    private static final String TAG = "QuickContact";
158
159    private static final String KEY_THEME_COLOR = "theme_color";
160
161    private static final int ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION = 150;
162    private static final int REQUEST_CODE_CONTACT_EDITOR_ACTIVITY = 1;
163    private static final int SCRIM_COLOR = Color.argb(0xB2, 0, 0, 0);
164    private static final String SCHEME_SMSTO = "smsto";
165    private static final String MIMETYPE_SMS = "vnd.android-dir/mms-sms";
166
167    /** This is the Intent action to install a shortcut in the launcher. */
168    private static final String ACTION_INSTALL_SHORTCUT =
169            "com.android.launcher.action.INSTALL_SHORTCUT";
170
171    @SuppressWarnings("deprecation")
172    private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY;
173
174    private Uri mLookupUri;
175    private String[] mExcludeMimes;
176    private int mExtraMode;
177    private int mStatusBarColor;
178    private boolean mHasAlreadyBeenOpened;
179
180    private ImageView mPhotoView;
181    private View mTransparentView;
182    private ExpandingEntryCardView mContactCard;
183    private ExpandingEntryCardView mRecentCard;
184    private ExpandingEntryCardView mAboutCard;
185    /**
186     * This list contains all the {@link DataItem}s. Each nested list contains all data items of a
187     * specific mimetype in sorted order, using mWithinMimeTypeDataItemComparator. The mimetype
188     * lists are sorted using mAmongstMimeTypeDataItemComparator.
189     */
190    private List<List<DataItem>> mDataItemsList;
191    /**
192     * A map between a mimetype string and the corresponding list of data items. The data items
193     * are in sorted order using mWithinMimeTypeDataItemComparator.
194     */
195    private Map<String, List<DataItem>> mDataItemsMap;
196    private MultiShrinkScroller mScroller;
197    private SelectAccountDialogFragmentListener mSelectAccountFragmentListener;
198    private AsyncTask<Void, Void, Pair<List<List<DataItem>>, Map<String, List<DataItem>>>>
199            mEntriesAndActionsTask;
200    private ColorDrawable mWindowScrim;
201    private boolean mIsWaitingForOtherPieceOfExitAnimation;
202    private boolean mIsExitAnimationInProgress;
203    private boolean mHasComputedThemeColor;
204    private ComponentName mSmsComponent;
205
206    private static final int MIN_NUM_CONTACT_ENTRIES_SHOWN = 3;
207    private static final int MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN = 3;
208
209    private Contact mContactData;
210    private ContactLoader mContactLoader;
211    private PorterDuffColorFilter mColorFilter;
212
213    private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter();
214
215    /**
216     * {@link #LEADING_MIMETYPES} and {@link #TRAILING_MIMETYPES} are used to sort MIME-types.
217     *
218     * <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog,
219     * in the order specified here.</p>
220     *
221     * <p>The ones in {@link #TRAILING_MIMETYPES} appear in the end of the dialog, in the order
222     * specified here.</p>
223     *
224     * <p>The rest go between them, in the order in the array.</p>
225     */
226    private static final List<String> LEADING_MIMETYPES = Lists.newArrayList(
227            Phone.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE);
228
229    /** See {@link #LEADING_MIMETYPES}. */
230    private static final List<String> TRAILING_MIMETYPES = Lists.newArrayList(
231            StructuredPostal.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE);
232
233    private static final List<String> ABOUT_CARD_MIMETYPES = Lists.newArrayList(
234            Event.CONTENT_ITEM_TYPE, GroupMembership.CONTENT_ITEM_TYPE, Identity.CONTENT_ITEM_TYPE,
235            Im.CONTENT_ITEM_TYPE, Nickname.CONTENT_ITEM_TYPE, Note.CONTENT_ITEM_TYPE,
236            Organization.CONTENT_ITEM_TYPE, Relation.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE);
237
238    /** Id for the background contact loader */
239    private static final int LOADER_CONTACT_ID = 0;
240
241    private static final String KEY_LOADER_EXTRA_PHONES =
242            QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_PHONES";
243
244    /** Id for the background Sms Loader */
245    private static final int LOADER_SMS_ID = 1;
246    private static final int MAX_SMS_RETRIEVE = 3;
247
248    /** Id for the back Calendar Loader */
249    private static final int LOADER_CALENDAR_ID = 2;
250    private static final String KEY_LOADER_EXTRA_EMAILS =
251            QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_EMAILS";
252    private static final int MAX_PAST_CALENDAR_RETRIEVE = 3;
253    private static final int MAX_FUTURE_CALENDAR_RETRIEVE = 3;
254    private static final long PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR =
255            180L * 24L * 60L * 60L * 1000L /* 180 days */;
256    private static final long FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR =
257            36L * 60L * 60L * 1000L /* 36 hours */;
258
259    /** Id for the background Call Log Loader */
260    private static final int LOADER_CALL_LOG_ID = 3;
261    private static final int MAX_CALL_LOG_RETRIEVE = 3;
262
263
264    private static final int[] mRecentLoaderIds = new int[]{
265        LOADER_SMS_ID,
266        LOADER_CALENDAR_ID,
267        LOADER_CALL_LOG_ID};
268    private Map<Integer, List<ContactInteraction>> mRecentLoaderResults;
269
270    private static final String FRAGMENT_TAG_SELECT_ACCOUNT = "select_account_fragment";
271
272    final OnClickListener mEntryClickHandler = new OnClickListener() {
273        @Override
274        public void onClick(View v) {
275            // Data Id is stored as the entry view id
276            final int dataId = v.getId();
277            Object intentObject = v.getTag();
278            if (intentObject == null || !(intentObject instanceof Intent)) {
279                Log.w(TAG, "Intent tag was not used correctly");
280                return;
281            }
282            final Intent intent = (Intent) intentObject;
283
284            // Default to USAGE_TYPE_CALL. Usage is summed among all types for sorting each data id
285            // so the exact usage type is not necessary in all cases
286            String usageType = DataUsageFeedback.USAGE_TYPE_CALL;
287
288            final String scheme = intent.getData().getScheme();
289            if ((scheme != null && scheme.equals(SCHEME_SMSTO)) ||
290                    (intent.getType() != null && intent.getType().equals(MIMETYPE_SMS))) {
291                usageType = DataUsageFeedback.USAGE_TYPE_SHORT_TEXT;
292            }
293
294            // Data IDs start at 1 so anything less is invalid
295            if (dataId > 0) {
296                final Uri uri = DataUsageFeedback.FEEDBACK_URI.buildUpon()
297                        .appendPath(String.valueOf(dataId))
298                        .appendQueryParameter(DataUsageFeedback.USAGE_TYPE, usageType)
299                        .build();
300                final boolean successful = getContentResolver().update(
301                        uri, new ContentValues(), null, null) > 0;
302                if (!successful) {
303                    Log.w(TAG, "DataUsageFeedback increment failed");
304                }
305            } else {
306                Log.w(TAG, "Invalid Data ID");
307            }
308
309            startActivity(intent);
310        }
311    };
312
313    final ExpandingEntryCardViewListener mExpandingEntryCardViewListener
314            = new ExpandingEntryCardViewListener() {
315        @Override
316        public void onCollapse(int heightDelta) {
317            mScroller.prepareForShrinkingScrollChild(heightDelta);
318        }
319    };
320
321    /**
322     * Headless fragment used to handle account selection callbacks invoked from
323     * {@link DirectoryContactUtil}.
324     */
325    public static class SelectAccountDialogFragmentListener extends Fragment
326            implements SelectAccountDialogFragment.Listener {
327
328        private QuickContactActivity mQuickContactActivity;
329
330        public SelectAccountDialogFragmentListener() {}
331
332        @Override
333        public void onAccountChosen(AccountWithDataSet account, Bundle extraArgs) {
334            DirectoryContactUtil.createCopy(mQuickContactActivity.mContactData.getContentValues(),
335                    account, mQuickContactActivity);
336        }
337
338        @Override
339        public void onAccountSelectorCancelled() {}
340
341        /**
342         * Set the parent activity. Since rotation can cause this fragment to be used across
343         * more than one activity instance, we need to explicitly set this value instead
344         * of making this class non-static.
345         */
346        public void setQuickContactActivity(QuickContactActivity quickContactActivity) {
347            mQuickContactActivity = quickContactActivity;
348        }
349    }
350
351    final MultiShrinkScrollerListener mMultiShrinkScrollerListener
352            = new MultiShrinkScrollerListener() {
353        @Override
354        public void onScrolledOffBottom() {
355            if (!mIsWaitingForOtherPieceOfExitAnimation) {
356                finish();
357                return;
358            }
359            mIsWaitingForOtherPieceOfExitAnimation = false;
360        }
361
362        @Override
363        public void onEnterFullscreen() {
364            updateStatusBarColor();
365        }
366
367        @Override
368        public void onExitFullscreen() {
369            updateStatusBarColor();
370        }
371
372        @Override
373        public void onStartScrollOffBottom() {
374            // Remove the window shim now that we are starting an Activity exit animation.
375            final int duration = getResources().getInteger(android.R.integer.config_shortAnimTime);
376            final ObjectAnimator animator = ObjectAnimator.ofInt(mWindowScrim, "alpha", 0xFF, 0);
377            animator.addListener(mExitWindowShimAnimationListener);
378            animator.setDuration(duration).start();
379            mIsWaitingForOtherPieceOfExitAnimation = true;
380            mIsExitAnimationInProgress = true;
381        }
382    };
383
384    final AnimatorListener mExitWindowShimAnimationListener = new AnimatorListenerAdapter() {
385        @Override
386        public void onAnimationEnd(Animator animation) {
387            if (!mIsWaitingForOtherPieceOfExitAnimation) {
388                finish();
389                return;
390            }
391            mIsWaitingForOtherPieceOfExitAnimation = false;
392        }
393    };
394
395
396    /**
397     * Data items are compared to the same mimetype based off of three qualities:
398     * 1. Super primary
399     * 2. Primary
400     * 3. Times used
401     */
402    private final Comparator<DataItem> mWithinMimeTypeDataItemComparator =
403            new Comparator<DataItem>() {
404        @Override
405        public int compare(DataItem lhs, DataItem rhs) {
406            if (!lhs.getMimeType().equals(rhs.getMimeType())) {
407                Log.wtf(TAG, "Comparing DataItems with different mimetypes lhs.getMimeType(): " +
408                        lhs.getMimeType() + " rhs.getMimeType(): " + rhs.getMimeType());
409                return 0;
410            }
411
412            if (lhs.isSuperPrimary()) {
413                return -1;
414            } else if (rhs.isSuperPrimary()) {
415                return 1;
416            } else if (lhs.isPrimary() && !rhs.isPrimary()) {
417                return -1;
418            } else if (!lhs.isPrimary() && rhs.isPrimary()) {
419                return 1;
420            } else {
421                final int lhsTimesUsed =
422                        lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed();
423                final int rhsTimesUsed =
424                        rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed();
425
426                return rhsTimesUsed - lhsTimesUsed;
427            }
428        }
429    };
430
431    private final Comparator<List<DataItem>> mAmongstMimeTypeDataItemComparator =
432            new Comparator<List<DataItem>> () {
433        @Override
434        public int compare(List<DataItem> lhsList, List<DataItem> rhsList) {
435            DataItem lhs = lhsList.get(0);
436            DataItem rhs = rhsList.get(0);
437            final int lhsTimesUsed = lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed();
438            final int rhsTimesUsed = rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed();
439            final int timesUsedDifference = rhsTimesUsed - lhsTimesUsed;
440            if (timesUsedDifference != 0) {
441                return timesUsedDifference;
442            }
443
444            final long lhsLastTimeUsed =
445                    lhs.getLastTimeUsed() == null ? 0 : lhs.getLastTimeUsed();
446            final long rhsLastTimeUsed =
447                    rhs.getLastTimeUsed() == null ? 0 : rhs.getLastTimeUsed();
448            final long lastTimeUsedDifference = rhsLastTimeUsed - lhsLastTimeUsed;
449            if (lastTimeUsedDifference > 0) {
450                return 1;
451            } else if (lastTimeUsedDifference < 0) {
452                return -1;
453            }
454
455            // Times used and last time used are the same. Resort to statically defined.
456            final String lhsMimeType = lhs.getMimeType();
457            final String rhsMimeType = rhs.getMimeType();
458            for (String mimeType : LEADING_MIMETYPES) {
459                if (lhsMimeType.equals(mimeType)) {
460                    return -1;
461                } else if (rhsMimeType.equals(mimeType)) {
462                    return 1;
463                }
464            }
465            // Trailing types come last, so flip the returns
466            for (String mimeType : TRAILING_MIMETYPES) {
467                if (lhsMimeType.equals(mimeType)) {
468                    return 1;
469                } else if (rhsMimeType.equals(mimeType)) {
470                    return -1;
471                }
472            }
473            return 0;
474        }
475    };
476
477    @Override
478    protected void onCreate(Bundle savedInstanceState) {
479        Trace.beginSection("onCreate()");
480        super.onCreate(savedInstanceState);
481
482        getWindow().setStatusBarColor(Color.TRANSPARENT);
483
484        processIntent(getIntent());
485
486        // Show QuickContact in front of soft input
487        getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
488                WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
489
490        setContentView(R.layout.quickcontact_activity);
491
492        mSmsComponent = PhoneCapabilityTester.getSmsComponent(this);
493
494        mContactCard = (ExpandingEntryCardView) findViewById(R.id.communication_card);
495        mRecentCard = (ExpandingEntryCardView) findViewById(R.id.recent_card);
496        mAboutCard = (ExpandingEntryCardView) findViewById(R.id.about_card);
497        mScroller = (MultiShrinkScroller) findViewById(R.id.multiscroller);
498
499        mContactCard.setOnClickListener(mEntryClickHandler);
500        mContactCard.setTitle(getResources().getString(R.string.communication_card_title));
501        mContactCard.setExpandButtonText(
502        getResources().getString(R.string.expanding_entry_card_view_see_all));
503
504        mRecentCard.setOnClickListener(mEntryClickHandler);
505        mRecentCard.setTitle(getResources().getString(R.string.recent_card_title));
506
507        mAboutCard.setOnClickListener(mEntryClickHandler);
508
509        mPhotoView = (ImageView) findViewById(R.id.photo);
510        mTransparentView = findViewById(R.id.transparent_view);
511        if (mScroller != null) {
512            mTransparentView.setOnClickListener(new OnClickListener() {
513                @Override
514                public void onClick(View v) {
515                    mScroller.scrollOffBottom();
516                }
517            });
518        }
519
520        final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
521        setActionBar(toolbar);
522        getActionBar().setTitle(null);
523        // Put a TextView with a known resource id into the ActionBar. This allows us to easily
524        // find the correct TextView location & size later.
525        toolbar.addView(getLayoutInflater().inflate(R.layout.quickcontact_title_placeholder, null));
526
527        mHasAlreadyBeenOpened = savedInstanceState != null;
528
529        mWindowScrim = new ColorDrawable(SCRIM_COLOR);
530        getWindow().setBackgroundDrawable(mWindowScrim);
531        if (!mHasAlreadyBeenOpened) {
532            final int duration = getResources().getInteger(android.R.integer.config_shortAnimTime);
533            ObjectAnimator.ofInt(mWindowScrim, "alpha", 0, 0xFF).setDuration(duration).start();
534        }
535
536        mScroller.initialize(mMultiShrinkScrollerListener, mExtraMode == MODE_FULLY_EXPANDED);
537        // mScroller needs to perform asynchronous measurements after initalize(), therefore
538        // we can't mark this as GONE.
539        mScroller.setVisibility(View.INVISIBLE);
540
541        setHeaderNameText(R.string.missing_name);
542
543        mSelectAccountFragmentListener= (SelectAccountDialogFragmentListener) getFragmentManager()
544                .findFragmentByTag(FRAGMENT_TAG_SELECT_ACCOUNT);
545        if (mSelectAccountFragmentListener == null) {
546            mSelectAccountFragmentListener = new SelectAccountDialogFragmentListener();
547            getFragmentManager().beginTransaction().add(0, mSelectAccountFragmentListener,
548                    FRAGMENT_TAG_SELECT_ACCOUNT).commit();
549            mSelectAccountFragmentListener.setRetainInstance(true);
550        }
551        mSelectAccountFragmentListener.setQuickContactActivity(this);
552
553        if (savedInstanceState != null) {
554            final int color = savedInstanceState.getInt(KEY_THEME_COLOR, 0);
555            SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false,
556                    new Runnable() {
557                        @Override
558                        public void run() {
559                            // Need to wait for the pre draw before setting the initial scroll
560                            // value. Prior to pre draw all scroll values are invalid.
561                            if (mHasAlreadyBeenOpened) {
562                                mScroller.setVisibility(View.VISIBLE);
563                                mScroller.setScroll(mScroller.getScrollNeededToBeFullScreen());
564                            }
565                            // Need to wait for pre draw for setting the theme color. Setting the
566                            // header tint before the MultiShrinkScroller has been measured will
567                            // cause incorrect tinting calculations.
568                            if (color != 0) {
569                                setThemeColor(MaterialColorMapUtils.calculateSecondaryColor(color));
570                            }
571                        }
572                    });
573        }
574
575        Trace.endSection();
576    }
577
578    protected void onActivityResult(int requestCode, int resultCode,
579            Intent data) {
580        if (requestCode == REQUEST_CODE_CONTACT_EDITOR_ACTIVITY &&
581                resultCode == ContactDeletionInteraction.RESULT_CODE_DELETED) {
582            // The contact that we were showing has been deleted.
583            finish();
584        }
585    }
586
587    @Override
588    protected void onNewIntent(Intent intent) {
589        super.onNewIntent(intent);
590        mHasAlreadyBeenOpened = true;
591        mHasComputedThemeColor = false;
592        processIntent(intent);
593    }
594
595    @Override
596    public void onSaveInstanceState(Bundle savedInstanceState) {
597        super.onSaveInstanceState(savedInstanceState);
598        if (mColorFilter != null) {
599            savedInstanceState.putInt(KEY_THEME_COLOR, mColorFilter.getColor());
600        }
601    }
602
603    private void processIntent(Intent intent) {
604        Uri lookupUri = intent.getData();
605
606        // Check to see whether it comes from the old version.
607        if (lookupUri != null && LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) {
608            final long rawContactId = ContentUris.parseId(lookupUri);
609            lookupUri = RawContacts.getContactLookupUri(getContentResolver(),
610                    ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
611        }
612        mExtraMode = getIntent().getIntExtra(QuickContact.EXTRA_MODE,
613                QuickContact.MODE_LARGE);
614        final Uri oldLookupUri = mLookupUri;
615
616        mLookupUri = Preconditions.checkNotNull(lookupUri, "missing lookupUri");
617        mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES);
618        if (oldLookupUri == null) {
619            mContactLoader = (ContactLoader) getLoaderManager().initLoader(
620                    LOADER_CONTACT_ID, null, mLoaderContactCallbacks);
621        } else if (oldLookupUri != mLookupUri) {
622            // After copying a directory contact, the contact URI changes. Therefore,
623            // we need to restart the loader and reload the new contact.
624            mContactLoader = (ContactLoader) getLoaderManager().restartLoader(
625                    LOADER_CONTACT_ID, null, mLoaderContactCallbacks);
626            for (int interactionLoaderId : mRecentLoaderIds) {
627                getLoaderManager().destroyLoader(interactionLoaderId);
628            }
629        }
630
631        NfcHandler.register(this, mLookupUri);
632    }
633
634    private void runEntranceAnimation() {
635        if (mHasAlreadyBeenOpened) {
636            return;
637        }
638        mHasAlreadyBeenOpened = true;
639        mScroller.scrollUpForEntranceAnimation(mExtraMode != MODE_FULLY_EXPANDED);
640    }
641
642    /** Assign this string to the view if it is not empty. */
643    private void setHeaderNameText(int resId) {
644        if (mScroller != null) {
645            mScroller.setTitle(getText(resId) == null ? null : getText(resId).toString());
646        }
647    }
648
649    /** Assign this string to the view if it is not empty. */
650    private void setHeaderNameText(String value) {
651        if (!TextUtils.isEmpty(value)) {
652            if (mScroller != null) {
653                mScroller.setTitle(value);
654            }
655        }
656    }
657
658    /**
659     * Check if the given MIME-type appears in the list of excluded MIME-types
660     * that the most-recent caller requested.
661     */
662    private boolean isMimeExcluded(String mimeType) {
663        if (mExcludeMimes == null) return false;
664        for (String excludedMime : mExcludeMimes) {
665            if (TextUtils.equals(excludedMime, mimeType)) {
666                return true;
667            }
668        }
669        return false;
670    }
671
672    /**
673     * Handle the result from the ContactLoader
674     */
675    private void bindContactData(final Contact data) {
676        Trace.beginSection("bindContactData");
677        mContactData = data;
678        invalidateOptionsMenu();
679
680        Trace.endSection();
681        Trace.beginSection("Set display photo & name");
682
683        mPhotoSetter.setupContactPhoto(data, mPhotoView);
684        extractAndApplyTintFromPhotoViewAsynchronously();
685        analyzeWhitenessOfPhotoAsynchronously();
686        setHeaderNameText(data.getDisplayName());
687
688        Trace.endSection();
689
690        mEntriesAndActionsTask = new AsyncTask<Void, Void,
691                Pair<List<List<DataItem>>, Map<String, List<DataItem>>>>() {
692
693            @Override
694            protected Pair<List<List<DataItem>>, Map<String, List<DataItem>>> doInBackground(
695                    Void... params) {
696                return generateDataModelFromContact(data);
697            }
698
699            @Override
700            protected void onPostExecute(Pair<List<List<DataItem>>,
701                    Map<String, List<DataItem>>> dataItemsPair) {
702                super.onPostExecute(dataItemsPair);
703                mDataItemsList = dataItemsPair.first;
704                mDataItemsMap = dataItemsPair.second;
705                // Check that original AsyncTask parameters are still valid and the activity
706                // is still running before binding to UI. A new intent could invalidate
707                // the results, for example.
708                if (data == mContactData && !isCancelled()) {
709                    bindDataToCards();
710                    showActivity();
711                }
712            }
713        };
714        mEntriesAndActionsTask.execute();
715    }
716
717    private void bindDataToCards() {
718        startInteractionLoaders();
719        populateContactAndAboutCard();
720    }
721
722    private void startInteractionLoaders() {
723        final List<DataItem> phoneDataItems = mDataItemsMap.get(Phone.CONTENT_ITEM_TYPE);
724        String[] phoneNumbers = null;
725        if (phoneDataItems != null) {
726            phoneNumbers = new String[phoneDataItems.size()];
727            for (int i = 0; i < phoneDataItems.size(); ++i) {
728                phoneNumbers[i] = ((PhoneDataItem) phoneDataItems.get(i)).getNumber();
729            }
730        }
731        final Bundle phonesExtraBundle = new Bundle();
732        phonesExtraBundle.putStringArray(KEY_LOADER_EXTRA_PHONES, phoneNumbers);
733
734        Trace.beginSection("start sms loader");
735        getLoaderManager().initLoader(
736                LOADER_SMS_ID,
737                phonesExtraBundle,
738                mLoaderInteractionsCallbacks);
739        Trace.endSection();
740
741        Trace.beginSection("start call log loader");
742        getLoaderManager().initLoader(
743                LOADER_CALL_LOG_ID,
744                phonesExtraBundle,
745                mLoaderInteractionsCallbacks);
746        Trace.endSection();
747
748
749        Trace.beginSection("start calendar loader");
750        final List<DataItem> emailDataItems = mDataItemsMap.get(Email.CONTENT_ITEM_TYPE);
751        String[] emailAddresses = null;
752        if (emailDataItems != null) {
753            emailAddresses = new String[emailDataItems.size()];
754            for (int i = 0; i < emailDataItems.size(); ++i) {
755                emailAddresses[i] = ((EmailDataItem) emailDataItems.get(i)).getAddress();
756            }
757        }
758        final Bundle emailsExtraBundle = new Bundle();
759        emailsExtraBundle.putStringArray(KEY_LOADER_EXTRA_EMAILS, emailAddresses);
760        getLoaderManager().initLoader(
761                LOADER_CALENDAR_ID,
762                emailsExtraBundle,
763                mLoaderInteractionsCallbacks);
764        Trace.endSection();
765    }
766
767    private void showActivity() {
768        if (mScroller != null) {
769            mScroller.setVisibility(View.VISIBLE);
770            SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false,
771                    new Runnable() {
772                        @Override
773                        public void run() {
774                            runEntranceAnimation();
775                        }
776                    });
777        }
778    }
779
780    private void populateContactAndAboutCard() {
781        Trace.beginSection("bind contact card");
782
783        final List<Entry> contactCardEntries = new ArrayList<>();
784        final List<Entry> aboutCardEntries = new ArrayList<>();
785
786        int topContactIndex = 0;
787        for (int i = 0; i < mDataItemsList.size(); ++i) {
788            final List<DataItem> dataItemsByMimeType = mDataItemsList.get(i);
789            final DataItem topDataItem = dataItemsByMimeType.get(0);
790            if (ABOUT_CARD_MIMETYPES.contains(topDataItem.getMimeType())) {
791                aboutCardEntries.addAll(dataItemsToEntries(mDataItemsList.get(i)));
792            } else {
793                // Add most used to the top of the contact card
794                final Entry topEntry = dataItemToEntry(topDataItem);
795                if (topEntry != null) {
796                    contactCardEntries.add(topContactIndex++, dataItemToEntry(topDataItem));
797                }
798                // TODO merge SMS into secondary action
799                if (topDataItem instanceof PhoneDataItem) {
800                    final PhoneDataItem phone = (PhoneDataItem) topDataItem;
801                    Intent smsIntent = null;
802                    if (mSmsComponent != null) {
803                        smsIntent = new Intent(Intent.ACTION_SENDTO,
804                                Uri.fromParts(CallUtil.SCHEME_SMSTO, phone.getNumber(), null));
805                        smsIntent.setComponent(mSmsComponent);
806                    }
807                    final int dataId = phone.getId() > Integer.MAX_VALUE ?
808                            -1 : (int) phone.getId();
809                    contactCardEntries.add(topContactIndex++,
810                            new Entry(dataId,
811                                    getResources().getDrawable(R.drawable.ic_message_24dp),
812                                    getResources().getString(R.string.send_message),
813                                    /* subHeader = */ null,
814                                    /* text = */ phone.buildDataString(
815                                            this, topDataItem.getDataKind()),
816                                            smsIntent,
817                                            /* isEditable = */ false));
818                }
819                // Add the rest of the entries to the bottom of the card
820                if (dataItemsByMimeType.size() > 1) {
821                    contactCardEntries.addAll(dataItemsToEntries(
822                            dataItemsByMimeType.subList(1, dataItemsByMimeType.size())));
823                }
824            }
825        }
826
827        if (contactCardEntries.size() > 0) {
828            mContactCard.initialize(contactCardEntries,
829                    /* numInitialVisibleEntries = */ MIN_NUM_CONTACT_ENTRIES_SHOWN,
830                    /* isExpanded = */ false,
831                    mExpandingEntryCardViewListener);
832            mContactCard.setVisibility(View.VISIBLE);
833        } else {
834            mContactCard.setVisibility(View.GONE);
835        }
836        Trace.endSection();
837
838        Trace.beginSection("bind about card");
839        mAboutCard.initialize(aboutCardEntries,
840                /* numInitialVisibleEntries = */ 1,
841                /* isExpanded = */ true,
842                mExpandingEntryCardViewListener);
843        Trace.endSection();
844    }
845
846    /**
847     * Builds the {@link DataItem}s Map out of the Contact.
848     * @param data The contact to build the data from.
849     * @return A pair containing a list of data items sorted within mimetype and sorted
850     *  amongst mimetype. The map goes from mimetype string to the sorted list of data items within
851     *  mimetype
852     */
853    private Pair<List<List<DataItem>>, Map<String, List<DataItem>>> generateDataModelFromContact(
854            Contact data) {
855        Trace.beginSection("Build data items map");
856
857        final Map<String, List<DataItem>> dataItemsMap = new HashMap<>();
858
859        final ResolveCache cache = ResolveCache.getInstance(this);
860        for (RawContact rawContact : data.getRawContacts()) {
861            for (DataItem dataItem : rawContact.getDataItems()) {
862                dataItem.setRawContactId(rawContact.getId());
863
864                final String mimeType = dataItem.getMimeType();
865                if (mimeType == null) continue;
866
867                final AccountType accountType = rawContact.getAccountType(this);
868                final DataKind dataKind = AccountTypeManager.getInstance(this)
869                        .getKindOrFallback(accountType, mimeType);
870                if (dataKind == null) continue;
871
872                dataItem.setDataKind(dataKind);
873
874                final boolean hasData = !TextUtils.isEmpty(dataItem.buildDataString(this,
875                        dataKind));
876
877                if (isMimeExcluded(mimeType) || !hasData) continue;
878
879                List<DataItem> dataItemListByType = dataItemsMap.get(mimeType);
880                if (dataItemListByType == null) {
881                    dataItemListByType = new ArrayList<>();
882                    dataItemsMap.put(mimeType, dataItemListByType);
883                }
884                dataItemListByType.add(dataItem);
885            }
886        }
887        Trace.endSection();
888
889        Trace.beginSection("sort within mimetypes");
890        /*
891         * Sorting is a multi part step. The end result is to a have a sorted list of the most
892         * used data items, one per mimetype. Then, within each mimetype, the list of data items
893         * for that type is also sorted, based off of {super primary, primary, times used} in that
894         * order.
895         */
896        final List<List<DataItem>> dataItemsList = new ArrayList<>();
897        for (List<DataItem> mimeTypeDataItems : dataItemsMap.values()) {
898            // Remove duplicate data items
899            Collapser.collapseList(mimeTypeDataItems, this);
900            // Sort within mimetype
901            Collections.sort(mimeTypeDataItems, mWithinMimeTypeDataItemComparator);
902            // Add to the list of data item lists
903            dataItemsList.add(mimeTypeDataItems);
904        }
905        Trace.endSection();
906
907        Trace.beginSection("sort amongst mimetypes");
908        // Sort amongst mimetypes to bubble up the top data items for the contact card
909        Collections.sort(dataItemsList, mAmongstMimeTypeDataItemComparator);
910        Trace.endSection();
911
912        return new Pair<>(dataItemsList, dataItemsMap);
913    }
914
915    /**
916     * Converts a {@link DataItem} into an {@link ExpandingEntryCardView.Entry} for display.
917     * If the {@link ExpandingEntryCardView.Entry} has no visual elements, null is returned.
918     * @param dataItem The {@link DataItem} to convert.
919     * @return The {@link ExpandingEntryCardView.Entry}, or null if no visual elements are present.
920     */
921    private Entry dataItemToEntry(DataItem dataItem) {
922        Drawable icon = null;
923        String header = null;
924        String subHeader = null;
925        Drawable subHeaderIcon = null;
926        String text = null;
927        Drawable textIcon = null;
928        Intent intent = null;
929        final boolean isEditable = false;
930
931        DataKind kind = dataItem.getDataKind();
932
933        if (dataItem instanceof ImDataItem) {
934            final ImDataItem im = (ImDataItem) dataItem;
935            intent = ContactsUtils.buildImIntent(this, im).first;
936            header = getResources().getString(R.string.header_im_entry);
937            final boolean isEmail = im.isCreatedFromEmail();
938            final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : im.getProtocol();
939            subHeader = Im.getProtocolLabel(getResources(), protocol,
940                    im.getCustomProtocol()).toString();
941        } else if (dataItem instanceof OrganizationDataItem) {
942            final OrganizationDataItem organization = (OrganizationDataItem) dataItem;
943            header = getResources().getString(R.string.header_organization_entry);
944            subHeader = organization.getCompany();
945            text = organization.getTitle();
946        } else if (dataItem instanceof NicknameDataItem) {
947            final NicknameDataItem nickname = (NicknameDataItem) dataItem;
948            // Build nickname entries
949            final boolean isNameRawContact =
950                (mContactData.getNameRawContactId() == dataItem.getRawContactId());
951
952            final boolean duplicatesTitle =
953                isNameRawContact
954                && mContactData.getDisplayNameSource() == DisplayNameSources.NICKNAME;
955
956            if (!duplicatesTitle) {
957                header = getResources().getString(R.string.header_nickname_entry);
958                subHeader = nickname.getName();
959            }
960        } else if (dataItem instanceof NoteDataItem) {
961            final NoteDataItem note = (NoteDataItem) dataItem;
962            header = getResources().getString(R.string.header_note_entry);
963            subHeader = note.getNote();
964        } else if (dataItem instanceof WebsiteDataItem) {
965            final WebsiteDataItem website = (WebsiteDataItem) dataItem;
966            header = getResources().getString(R.string.header_website_entry);
967            subHeader = website.getUrl();
968            try {
969                final WebAddress webAddress = new WebAddress(website.buildDataString(this, kind));
970                intent = new Intent(Intent.ACTION_VIEW, Uri.parse(webAddress.toString()));
971            } catch (final ParseException e) {
972                Log.e(TAG, "Couldn't parse website: " + website.buildDataString(this, kind));
973            }
974        } else if (dataItem instanceof EventDataItem) {
975            final EventDataItem event = (EventDataItem) dataItem;
976            final String dataString = event.buildDataString(this, kind);
977            final Calendar cal = DateUtils.parseDate(dataString, false);
978            if (cal != null) {
979                final Date nextAnniversary =
980                        DateUtils.getNextAnnualDate(cal);
981                final Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon();
982                builder.appendPath("time");
983                ContentUris.appendId(builder, nextAnniversary.getTime());
984                intent = new Intent(Intent.ACTION_VIEW).setData(builder.build());
985            }
986            header = getResources().getString(R.string.header_event_entry);
987            if (event.hasKindTypeColumn(kind)) {
988                subHeader = getResources().getString(Event.getTypeResource(
989                        event.getKindTypeColumn(kind)));
990            }
991            text = DateUtils.formatDate(this, dataString);
992        } else if (dataItem instanceof RelationDataItem) {
993            final RelationDataItem relation = (RelationDataItem) dataItem;
994            final String dataString = relation.buildDataString(this, kind);
995            if (!TextUtils.isEmpty(dataString)) {
996                intent = new Intent(Intent.ACTION_SEARCH);
997                intent.putExtra(SearchManager.QUERY, dataString);
998                intent.setType(Contacts.CONTENT_TYPE);
999            }
1000            header = getResources().getString(R.string.header_relation_entry);
1001            subHeader = relation.getName();
1002            if (relation.hasKindTypeColumn(kind)) {
1003                text = Relation.getTypeLabel(getResources(), relation.getKindTypeColumn(kind),
1004                        relation.getLabel()).toString();
1005            }
1006        } else if (dataItem instanceof PhoneDataItem) {
1007            final PhoneDataItem phone = (PhoneDataItem) dataItem;
1008            if (!TextUtils.isEmpty(phone.getNumber())) {
1009                header = phone.buildDataString(this, kind);
1010                if (phone.hasKindTypeColumn(kind)) {
1011                    text = Phone.getTypeLabel(getResources(), phone.getKindTypeColumn(kind),
1012                            phone.getLabel()).toString();
1013                }
1014                icon = getResources().getDrawable(R.drawable.ic_phone_24dp);
1015                if (PhoneCapabilityTester.isPhone(this)) {
1016                    intent = CallUtil.getCallIntent(phone.getNumber());
1017                }
1018            }
1019        } else if (dataItem instanceof EmailDataItem) {
1020            final EmailDataItem email = (EmailDataItem) dataItem;
1021            final String address = email.getData();
1022            if (!TextUtils.isEmpty(address)) {
1023                final Uri mailUri = Uri.fromParts(CallUtil.SCHEME_MAILTO, address, null);
1024                intent = new Intent(Intent.ACTION_SENDTO, mailUri);
1025                header = email.getAddress();
1026                if (email.hasKindTypeColumn(kind)) {
1027                    text = Email.getTypeLabel(getResources(), email.getKindTypeColumn(kind),
1028                            email.getLabel()).toString();
1029                }
1030                icon = getResources().getDrawable(R.drawable.ic_email_24dp);
1031            }
1032        } else if (dataItem instanceof StructuredPostalDataItem) {
1033            StructuredPostalDataItem postal = (StructuredPostalDataItem) dataItem;
1034            final String postalAddress = postal.getFormattedAddress();
1035            if (!TextUtils.isEmpty(postalAddress)) {
1036                intent = StructuredPostalUtils.getViewPostalAddressIntent(postalAddress);
1037                header = postal.getFormattedAddress();
1038                if (postal.hasKindTypeColumn(kind)) {
1039                    text = StructuredPostal.getTypeLabel(getResources(),
1040                            postal.getKindTypeColumn(kind), postal.getLabel()).toString();
1041                }
1042                icon = getResources().getDrawable(R.drawable.ic_place_24dp);
1043            }
1044        } else if (dataItem instanceof SipAddressDataItem) {
1045            if (PhoneCapabilityTester.isSipPhone(this)) {
1046                final SipAddressDataItem sip = (SipAddressDataItem) dataItem;
1047                final String address = sip.getSipAddress();
1048                if (!TextUtils.isEmpty(address)) {
1049                    final Uri callUri = Uri.fromParts(CallUtil.SCHEME_SIP, address, null);
1050                    intent = CallUtil.getCallIntent(callUri);
1051                    // Note that this item will get a SIP-specific variant
1052                    // of the "call phone" icon, rather than the standard
1053                    // app icon for the Phone app (which we show for
1054                    // regular phone numbers.)  That's because the phone
1055                    // app explicitly specifies an android:icon attribute
1056                    // for the SIP-related intent-filters in its manifest.
1057                }
1058                icon = ResolveCache.getInstance(this).getIcon(sip.getMimeType(), intent);
1059                // Call mutate to create a new Drawable.ConstantState for color filtering
1060                if (icon != null) {
1061                    icon.mutate();
1062                }
1063            }
1064        } else if (dataItem instanceof StructuredNameDataItem) {
1065            final String givenName = ((StructuredNameDataItem) dataItem).getGivenName();
1066            if (!TextUtils.isEmpty(givenName)) {
1067                mAboutCard.setTitle(getResources().getString(R.string.about_card_title) +
1068                        " " + givenName);
1069            } else {
1070                mAboutCard.setTitle(getResources().getString(R.string.about_card_title));
1071            }
1072        } else {
1073            // Custom DataItem
1074            header = dataItem.buildDataStringForDisplay(this, kind);
1075            text = kind.typeColumn;
1076            intent = new Intent(Intent.ACTION_VIEW);
1077            intent.setDataAndType(Uri.parse(dataItem.buildDataString(this, kind)),
1078                    dataItem.getMimeType());
1079            icon = ResolveCache.getInstance(this).getIcon(dataItem.getMimeType(), intent);
1080        }
1081
1082        if (intent != null) {
1083            // Do not set the intent is there are no resolves
1084            if (!PhoneCapabilityTester.isIntentRegistered(this, intent)) {
1085                intent = null;
1086            }
1087        }
1088
1089        // If the Entry has no visual elements, return null
1090        if (icon == null && TextUtils.isEmpty(header) && TextUtils.isEmpty(subHeader) &&
1091                subHeaderIcon == null && TextUtils.isEmpty(text) && textIcon == null) {
1092            return null;
1093        }
1094
1095        final int dataId = dataItem.getId() > Integer.MAX_VALUE ?
1096                -1 : (int) dataItem.getId();
1097
1098        return new Entry(dataId, icon, header, subHeader, subHeaderIcon, text, textIcon,
1099                intent, isEditable);
1100    }
1101
1102    private List<Entry> dataItemsToEntries(List<DataItem> dataItems) {
1103        final List<Entry> entries = new ArrayList<>();
1104        for (DataItem dataItem : dataItems) {
1105            final Entry entry = dataItemToEntry(dataItem);
1106            if (entry != null) {
1107                entries.add(entry);
1108            }
1109        }
1110        return entries;
1111    }
1112
1113    /**
1114     * Asynchronously extract the most vibrant color from the PhotoView. Once extracted,
1115     * apply this tint to {@link MultiShrinkScroller}. This operation takes about 20-30ms
1116     * on a Nexus 5.
1117     */
1118    private void extractAndApplyTintFromPhotoViewAsynchronously() {
1119        if (mScroller == null) {
1120            return;
1121        }
1122        final Drawable imageViewDrawable = mPhotoView.getDrawable();
1123        new AsyncTask<Void, Void, MaterialPalette>() {
1124            @Override
1125            protected MaterialPalette doInBackground(Void... params) {
1126
1127                if (imageViewDrawable instanceof BitmapDrawable) {
1128                    final Bitmap bitmap = ((BitmapDrawable) imageViewDrawable).getBitmap();
1129                    final int primaryColor = colorFromBitmap(bitmap);
1130                    if (primaryColor != 0) {
1131                        return MaterialColorMapUtils.calculatePrimaryAndSecondaryColor(
1132                                primaryColor);
1133                    }
1134                }
1135                if (imageViewDrawable instanceof LetterTileDrawable) {
1136                    final int primaryColor = ((LetterTileDrawable) imageViewDrawable).getColor();
1137                    return MaterialColorMapUtils.calculateSecondaryColor(primaryColor);
1138                }
1139                return MaterialColorMapUtils.calculatePrimaryAndSecondaryColor(
1140                        getResources().getColor(R.color.quickcontact_default_photo_tint_color));
1141            }
1142
1143            @Override
1144            protected void onPostExecute(MaterialPalette palette) {
1145                super.onPostExecute(palette);
1146                if (mHasComputedThemeColor) {
1147                    // If we had previously computed a theme color from the contact photo,
1148                    // then do not update the theme color. Changing the theme color several
1149                    // seconds after QC has started, as a result of an updated/upgraded photo,
1150                    // is a jarring experience. On the other hand, changing the theme color after
1151                    // a rotation or onNewIntent() is perfectly fine.
1152                    return;
1153                }
1154                // Check that the Photo has not changed. If it has changed, the new tint
1155                // color needs to be extracted
1156                if (imageViewDrawable == mPhotoView.getDrawable()) {
1157                    mHasComputedThemeColor = true;
1158                    setThemeColor(palette);
1159                }
1160            }
1161        }.execute();
1162    }
1163
1164    /**
1165     * Examine how many white pixels are in the bitmap in order to determine whether or not
1166     * we need gradient overlays on top of the image.
1167     */
1168    private void analyzeWhitenessOfPhotoAsynchronously() {
1169        final Drawable imageViewDrawable = mPhotoView.getDrawable();
1170        new AsyncTask<Void, Void, Boolean>() {
1171            @Override
1172            protected Boolean doInBackground(Void... params) {
1173                if (imageViewDrawable instanceof BitmapDrawable) {
1174                    final Bitmap bitmap = ((BitmapDrawable) imageViewDrawable).getBitmap();
1175                    return WhitenessUtils.isBitmapWhiteAtTopOrBottom(bitmap);
1176                }
1177                return !(imageViewDrawable instanceof LetterTileDrawable);
1178            }
1179
1180            @Override
1181            protected void onPostExecute(Boolean isWhite) {
1182                super.onPostExecute(isWhite);
1183                mScroller.setUseGradient(isWhite);
1184            }
1185        }.execute();
1186    }
1187
1188    private void setThemeColor(MaterialPalette palette) {
1189        // If the color is invalid, use the predefined default
1190        final int primaryColor = palette.mPrimaryColor;
1191        mScroller.setHeaderTintColor(primaryColor);
1192        mStatusBarColor = palette.mSecondaryColor;
1193        updateStatusBarColor();
1194
1195        mColorFilter =
1196                new PorterDuffColorFilter(primaryColor, PorterDuff.Mode.SRC_ATOP);
1197        mContactCard.setColorAndFilter(primaryColor, mColorFilter);
1198        mRecentCard.setColorAndFilter(primaryColor, mColorFilter);
1199        mAboutCard.setColorAndFilter(primaryColor, mColorFilter);
1200    }
1201
1202    private void updateStatusBarColor() {
1203        if (mScroller == null) {
1204            return;
1205        }
1206        final int desiredStatusBarColor;
1207        // Only use a custom status bar color if QuickContacts touches the top of the viewport.
1208        if (mScroller.getScrollNeededToBeFullScreen() <= 0) {
1209            desiredStatusBarColor = mStatusBarColor;
1210        } else {
1211            desiredStatusBarColor = Color.TRANSPARENT;
1212        }
1213        // Animate to the new color.
1214        if (desiredStatusBarColor != getWindow().getStatusBarColor()) {
1215            final ObjectAnimator animation = ObjectAnimator.ofInt(getWindow(), "statusBarColor",
1216                    getWindow().getStatusBarColor(), desiredStatusBarColor);
1217            animation.setDuration(ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION);
1218            animation.setEvaluator(new ArgbEvaluator());
1219            animation.start();
1220        }
1221    }
1222
1223    private int colorFromBitmap(Bitmap bitmap) {
1224        // Author of Palette recommends using 24 colors when analyzing profile photos.
1225        final int NUMBER_OF_PALETTE_COLORS = 24;
1226        final Palette palette = Palette.generate(bitmap, NUMBER_OF_PALETTE_COLORS);
1227        if (palette != null && palette.getVibrantSwatch() != null) {
1228            return palette.getVibrantSwatch().getRgb();
1229        }
1230        return 0;
1231    }
1232
1233    private List<Entry> contactInteractionsToEntries(List<ContactInteraction> interactions) {
1234        final List<Entry> entries = new ArrayList<>();
1235        for (ContactInteraction interaction : interactions) {
1236            entries.add(new Entry(/* id = */ -1,
1237                    interaction.getIcon(this),
1238                    interaction.getViewHeader(this),
1239                    interaction.getViewBody(this),
1240                    interaction.getBodyIcon(this),
1241                    interaction.getViewFooter(this),
1242                    interaction.getFooterIcon(this),
1243                    interaction.getIntent(),
1244                    /* isEditable = */ false));
1245        }
1246        return entries;
1247    }
1248
1249    private final LoaderCallbacks<Contact> mLoaderContactCallbacks =
1250            new LoaderCallbacks<Contact>() {
1251        @Override
1252        public void onLoaderReset(Loader<Contact> loader) {
1253        }
1254
1255        @Override
1256        public void onLoadFinished(Loader<Contact> loader, Contact data) {
1257            Trace.beginSection("onLoadFinished()");
1258
1259            if (isFinishing()) {
1260                return;
1261            }
1262            if (data.isError()) {
1263                // This shouldn't ever happen, so throw an exception. The {@link ContactLoader}
1264                // should log the actual exception.
1265                throw new IllegalStateException("Failed to load contact", data.getException());
1266            }
1267            if (data.isNotFound()) {
1268                if (mHasAlreadyBeenOpened) {
1269                    finish();
1270                } else {
1271                    Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri());
1272                    Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage,
1273                            Toast.LENGTH_LONG).show();
1274                }
1275                return;
1276            }
1277
1278            bindContactData(data);
1279
1280            Trace.endSection();
1281        }
1282
1283        @Override
1284        public Loader<Contact> onCreateLoader(int id, Bundle args) {
1285            if (mLookupUri == null) {
1286                Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early");
1287            }
1288            // Load all contact data. We need loadGroupMetaData=true to determine whether the
1289            // contact is invisible. If it is, we need to display an "Add to Contacts" MenuItem.
1290            return new ContactLoader(getApplicationContext(), mLookupUri,
1291                    true /*loadGroupMetaData*/, false /*loadInvitableAccountTypes*/,
1292                    true /*postViewNotification*/, true /*computeFormattedPhoneNumber*/);
1293        }
1294    };
1295
1296    @Override
1297    public void onBackPressed() {
1298        if (mScroller != null) {
1299            if (!mIsExitAnimationInProgress) {
1300                mScroller.scrollOffBottom();
1301            }
1302        } else {
1303            super.onBackPressed();
1304        }
1305    }
1306
1307    @Override
1308    public void finish() {
1309        super.finish();
1310
1311        // override transitions to skip the standard window animations
1312        overridePendingTransition(0, 0);
1313    }
1314
1315    private final LoaderCallbacks<List<ContactInteraction>> mLoaderInteractionsCallbacks =
1316            new LoaderCallbacks<List<ContactInteraction>>() {
1317
1318        @Override
1319        public Loader<List<ContactInteraction>> onCreateLoader(int id, Bundle args) {
1320            Log.v(TAG, "onCreateLoader");
1321            Loader<List<ContactInteraction>> loader = null;
1322            switch (id) {
1323                case LOADER_SMS_ID:
1324                    Log.v(TAG, "LOADER_SMS_ID");
1325                    loader = new SmsInteractionsLoader(
1326                            QuickContactActivity.this,
1327                            args.getStringArray(KEY_LOADER_EXTRA_PHONES),
1328                            MAX_SMS_RETRIEVE);
1329                    break;
1330                case LOADER_CALENDAR_ID:
1331                    Log.v(TAG, "LOADER_CALENDAR_ID");
1332                    final String[] emailsArray = args.getStringArray(KEY_LOADER_EXTRA_EMAILS);
1333                    List<String> emailsList = null;
1334                    if (emailsArray != null) {
1335                        emailsList = Arrays.asList(args.getStringArray(KEY_LOADER_EXTRA_EMAILS));
1336                    }
1337                    loader = new CalendarInteractionsLoader(
1338                            QuickContactActivity.this,
1339                            emailsList,
1340                            MAX_FUTURE_CALENDAR_RETRIEVE,
1341                            MAX_PAST_CALENDAR_RETRIEVE,
1342                            FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR,
1343                            PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR);
1344                    break;
1345                case LOADER_CALL_LOG_ID:
1346                    Log.v(TAG, "LOADER_CALL_LOG_ID");
1347                    loader = new CallLogInteractionsLoader(
1348                            QuickContactActivity.this,
1349                            args.getStringArray(KEY_LOADER_EXTRA_PHONES),
1350                            MAX_CALL_LOG_RETRIEVE);
1351            }
1352            return loader;
1353        }
1354
1355        @Override
1356        public void onLoadFinished(Loader<List<ContactInteraction>> loader,
1357                List<ContactInteraction> data) {
1358            if (mRecentLoaderResults == null) {
1359                mRecentLoaderResults = new HashMap<Integer, List<ContactInteraction>>();
1360            }
1361            Log.v(TAG, "onLoadFinished ~ loader.getId() " + loader.getId() + " data.size() " +
1362                    data.size());
1363            mRecentLoaderResults.put(loader.getId(), data);
1364
1365            if (isAllRecentDataLoaded()) {
1366                bindRecentData();
1367            }
1368        }
1369
1370        @Override
1371        public void onLoaderReset(Loader<List<ContactInteraction>> loader) {
1372            mRecentLoaderResults.remove(loader.getId());
1373        }
1374
1375    };
1376
1377    private boolean isAllRecentDataLoaded() {
1378        return mRecentLoaderResults.size() == mRecentLoaderIds.length;
1379    }
1380
1381    private void bindRecentData() {
1382        final List<ContactInteraction> allInteractions = new ArrayList<>();
1383        for (List<ContactInteraction> loaderInteractions : mRecentLoaderResults.values()) {
1384            allInteractions.addAll(loaderInteractions);
1385        }
1386
1387        // Sort the interactions by most recent
1388        Collections.sort(allInteractions, new Comparator<ContactInteraction>() {
1389            @Override
1390            public int compare(ContactInteraction a, ContactInteraction b) {
1391                return a.getInteractionDate() >= b.getInteractionDate() ? -1 : 1;
1392            }
1393        });
1394
1395        if (allInteractions.size() > 0) {
1396            mRecentCard.initialize(contactInteractionsToEntries(allInteractions),
1397                    /* numInitialVisibleEntries = */ MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN,
1398                    /* isExpanded = */ false, mExpandingEntryCardViewListener);
1399            mRecentCard.setVisibility(View.VISIBLE);
1400        }
1401
1402        // About card is initialized along with the contact card, but since it appears after
1403        // the recent card in the UI, we hold off until making it visible until the recent card
1404        // is also ready to avoid stuttering.
1405        if (mAboutCard.shouldShow()) {
1406            mAboutCard.setVisibility(View.VISIBLE);
1407        } else {
1408            mAboutCard.setVisibility(View.GONE);
1409        }
1410    }
1411
1412    @Override
1413    protected void onStop() {
1414        super.onStop();
1415
1416        if (mEntriesAndActionsTask != null) {
1417            // Once the activity is stopped, we will no longer want to bind mEntriesAndActionsTask's
1418            // results on the UI thread. In some circumstances Activities are killed without
1419            // onStop() being called. This is not a problem, because in these circumstances
1420            // the entire process will be killed.
1421            mEntriesAndActionsTask.cancel(/* mayInterruptIfRunning = */ false);
1422        }
1423    }
1424
1425    /**
1426     * Returns true if it is possible to edit the current contact.
1427     */
1428    private boolean isContactEditable() {
1429        return mContactData != null && !mContactData.isDirectoryEntry();
1430    }
1431
1432    private void editContact() {
1433        final Intent intent = new Intent(Intent.ACTION_EDIT, mLookupUri);
1434        mContactLoader.cacheResult();
1435        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
1436        startActivityForResult(intent, REQUEST_CODE_CONTACT_EDITOR_ACTIVITY);
1437    }
1438
1439    private void toggleStar(MenuItem starredMenuItem) {
1440        // Make sure there is a contact
1441        if (mLookupUri != null) {
1442            // Read the current starred value from the UI instead of using the last
1443            // loaded state. This allows rapid tapping without writing the same
1444            // value several times
1445            final boolean isStarred = starredMenuItem.isChecked();
1446
1447            // To improve responsiveness, swap out the picture (and tag) in the UI already
1448            ContactDetailDisplayUtils.configureStarredMenuItem(starredMenuItem,
1449                    mContactData.isDirectoryEntry(), mContactData.isUserProfile(),
1450                    !isStarred);
1451
1452            // Now perform the real save
1453            final Intent intent = ContactSaveService.createSetStarredIntent(
1454                    QuickContactActivity.this, mLookupUri, !isStarred);
1455            startService(intent);
1456        }
1457    }
1458
1459    /**
1460     * Calls into the contacts provider to get a pre-authorized version of the given URI.
1461     */
1462    private Uri getPreAuthorizedUri(Uri uri) {
1463        final Bundle uriBundle = new Bundle();
1464        uriBundle.putParcelable(ContactsContract.Authorization.KEY_URI_TO_AUTHORIZE, uri);
1465        final Bundle authResponse = getContentResolver().call(
1466                ContactsContract.AUTHORITY_URI,
1467                ContactsContract.Authorization.AUTHORIZATION_METHOD,
1468                null,
1469                uriBundle);
1470        if (authResponse != null) {
1471            return (Uri) authResponse.getParcelable(
1472                    ContactsContract.Authorization.KEY_AUTHORIZED_URI);
1473        } else {
1474            return uri;
1475        }
1476    }
1477
1478    private void shareContact() {
1479        final String lookupKey = mContactData.getLookupKey();
1480        Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey);
1481        if (mContactData.isUserProfile()) {
1482            // User is sharing the profile.  We don't want to force the receiver to have
1483            // the highly-privileged READ_PROFILE permission, so we need to request a
1484            // pre-authorized URI from the provider.
1485            shareUri = getPreAuthorizedUri(shareUri);
1486        }
1487
1488        final Intent intent = new Intent(Intent.ACTION_SEND);
1489        intent.setType(Contacts.CONTENT_VCARD_TYPE);
1490        intent.putExtra(Intent.EXTRA_STREAM, shareUri);
1491
1492        // Launch chooser to share contact via
1493        final CharSequence chooseTitle = getText(R.string.share_via);
1494        final Intent chooseIntent = Intent.createChooser(intent, chooseTitle);
1495
1496        try {
1497            this.startActivity(chooseIntent);
1498        } catch (final ActivityNotFoundException ex) {
1499            Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show();
1500        }
1501    }
1502
1503    /**
1504     * Creates a launcher shortcut with the current contact.
1505     */
1506    private void createLauncherShortcutWithContact() {
1507        final ShortcutIntentBuilder builder = new ShortcutIntentBuilder(this,
1508                new OnShortcutIntentCreatedListener() {
1509
1510                    @Override
1511                    public void onShortcutIntentCreated(Uri uri, Intent shortcutIntent) {
1512                        // Broadcast the shortcutIntent to the launcher to create a
1513                        // shortcut to this contact
1514                        shortcutIntent.setAction(ACTION_INSTALL_SHORTCUT);
1515                        QuickContactActivity.this.sendBroadcast(shortcutIntent);
1516
1517                        // Send a toast to give feedback to the user that a shortcut to this
1518                        // contact was added to the launcher.
1519                        Toast.makeText(QuickContactActivity.this,
1520                                R.string.createContactShortcutSuccessful,
1521                                Toast.LENGTH_SHORT).show();
1522                    }
1523
1524                });
1525        builder.createContactShortcutIntent(mLookupUri);
1526    }
1527
1528    @Override
1529    public boolean onCreateOptionsMenu(Menu menu) {
1530        final MenuInflater inflater = getMenuInflater();
1531        inflater.inflate(R.menu.quickcontact, menu);
1532        return true;
1533    }
1534
1535    @Override
1536    public boolean onPrepareOptionsMenu(Menu menu) {
1537        if (mContactData != null) {
1538            final MenuItem starredMenuItem = menu.findItem(R.id.menu_star);
1539            ContactDetailDisplayUtils.configureStarredMenuItem(starredMenuItem,
1540                    mContactData.isDirectoryEntry(), mContactData.isUserProfile(),
1541                    mContactData.getStarred());
1542            // Configure edit MenuItem
1543            final MenuItem editMenuItem = menu.findItem(R.id.menu_edit);
1544            editMenuItem.setVisible(true);
1545            if (DirectoryContactUtil.isDirectoryContact(mContactData) || InvisibleContactUtil
1546                    .isInvisibleAndAddable(mContactData, this)) {
1547                editMenuItem.setIcon(R.drawable.ic_person_add_tinted_24dp);
1548            } else if (isContactEditable()) {
1549                editMenuItem.setIcon(R.drawable.ic_create_24dp);
1550            } else {
1551                editMenuItem.setVisible(false);
1552            }
1553            return true;
1554        }
1555        return false;
1556    }
1557
1558    @Override
1559    public boolean onOptionsItemSelected(MenuItem item) {
1560        switch (item.getItemId()) {
1561            case R.id.menu_star:
1562                toggleStar(item);
1563                return true;
1564            case R.id.menu_edit:
1565                if (DirectoryContactUtil.isDirectoryContact(mContactData)) {
1566                    DirectoryContactUtil.addToMyContacts(mContactData, this, getFragmentManager(),
1567                            mSelectAccountFragmentListener);
1568                } else if (InvisibleContactUtil.isInvisibleAndAddable(mContactData, this)) {
1569                    InvisibleContactUtil.addToDefaultGroup(mContactData, this);
1570                } else if (isContactEditable()) {
1571                    editContact();
1572                }
1573                return true;
1574            case R.id.menu_share:
1575                shareContact();
1576                return true;
1577            case R.id.menu_create_contact_shortcut:
1578                createLauncherShortcutWithContact();
1579                return true;
1580            default:
1581                return super.onOptionsItemSelected(item);
1582        }
1583    }
1584}
1585