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