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