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