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