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