QuickContactActivity.java revision 8a6d0022b07640d4a1fb8b264c8822bbab2981ad
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.content.ActivityNotFoundException;
25import android.content.ContentUris;
26import android.content.Intent;
27import android.content.Loader;
28import android.content.pm.PackageManager;
29import android.graphics.Bitmap;
30import android.graphics.Color;
31import android.graphics.drawable.BitmapDrawable;
32import android.graphics.drawable.ColorDrawable;
33import android.graphics.drawable.Drawable;
34import android.graphics.PorterDuff;
35import android.graphics.PorterDuffColorFilter;
36import android.net.Uri;
37import android.os.AsyncTask;
38import android.os.Bundle;
39import android.os.Trace;
40import android.provider.ContactsContract;
41import android.provider.ContactsContract.CommonDataKinds.Email;
42import android.provider.ContactsContract.CommonDataKinds.Phone;
43import android.provider.ContactsContract.CommonDataKinds.SipAddress;
44import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
45import android.provider.ContactsContract.CommonDataKinds.Website;
46import android.provider.ContactsContract.Contacts;
47import android.provider.ContactsContract.QuickContact;
48import android.provider.ContactsContract.RawContacts;
49import android.support.v7.graphics.Palette;
50import android.text.TextUtils;
51import android.util.Log;
52import android.view.Menu;
53import android.view.MenuItem;
54import android.view.MenuInflater;
55import android.view.View;
56import android.view.View.OnClickListener;
57import android.view.WindowManager;
58import android.widget.ImageView;
59import android.widget.Toast;
60import android.widget.Toolbar;
61
62import com.android.contacts.ContactSaveService;
63import com.android.contacts.ContactsActivity;
64import com.android.contacts.common.Collapser;
65import com.android.contacts.R;
66import com.android.contacts.common.editor.SelectAccountDialogFragment;
67import com.android.contacts.common.lettertiles.LetterTileDrawable;
68import com.android.contacts.common.list.ShortcutIntentBuilder;
69import com.android.contacts.common.list.ShortcutIntentBuilder.OnShortcutIntentCreatedListener;
70import com.android.contacts.common.model.AccountTypeManager;
71import com.android.contacts.common.model.Contact;
72import com.android.contacts.common.model.ContactLoader;
73import com.android.contacts.common.model.RawContact;
74import com.android.contacts.common.model.account.AccountType;
75import com.android.contacts.common.model.account.AccountWithDataSet;
76import com.android.contacts.common.model.dataitem.DataItem;
77import com.android.contacts.common.model.dataitem.DataKind;
78import com.android.contacts.common.model.dataitem.EmailDataItem;
79import com.android.contacts.common.model.dataitem.ImDataItem;
80import com.android.contacts.common.model.dataitem.PhoneDataItem;
81import com.android.contacts.common.util.DataStatus;
82import com.android.contacts.detail.ContactDetailDisplayUtils;
83import com.android.contacts.common.util.UriUtils;
84import com.android.contacts.interactions.CalendarInteractionsLoader;
85import com.android.contacts.interactions.ContactDeletionInteraction;
86import com.android.contacts.interactions.ContactInteraction;
87import com.android.contacts.interactions.SmsInteractionsLoader;
88import com.android.contacts.quickcontact.ExpandingEntryCardView.Entry;
89import com.android.contacts.util.ImageViewDrawableSetter;
90import com.android.contacts.util.SchedulingUtils;
91import com.android.contacts.widget.MultiShrinkScroller;
92import com.android.contacts.widget.MultiShrinkScroller.MultiShrinkScrollerListener;
93
94import com.google.common.base.Preconditions;
95import com.google.common.collect.Lists;
96
97import java.util.ArrayList;
98import java.util.Arrays;
99import java.util.Collection;
100import java.util.Collections;
101import java.util.Comparator;
102import java.util.HashMap;
103import java.util.HashSet;
104import java.util.List;
105import java.util.Map;
106import java.util.Set;
107
108/**
109 * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads
110 * data asynchronously, and then shows a popup with details centered around
111 * {@link Intent#getSourceBounds()}.
112 */
113public class QuickContactActivity extends ContactsActivity {
114
115    /**
116     * QuickContacts immediately takes up the full screen. All possible information is shown.
117     * This value for {@link android.provider.ContactsContract.QuickContact#EXTRA_MODE}
118     * should only be used by the Contacts app.
119     */
120    public static final int MODE_FULLY_EXPANDED = 4;
121
122    private static final String TAG = "QuickContact";
123
124    private static final int ANIMATION_SLIDE_OPEN_DURATION = 250;
125    private static final int ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION = 75;
126    private static final int REQUEST_CODE_CONTACT_EDITOR_ACTIVITY = 1;
127    private static final float SYSTEM_BAR_BRIGHTNESS_FACTOR = 0.7f;
128    private static final int SHIM_COLOR = Color.argb(0x7F, 0, 0, 0);
129
130    /** This is the Intent action to install a shortcut in the launcher. */
131    private static final String ACTION_INSTALL_SHORTCUT =
132            "com.android.launcher.action.INSTALL_SHORTCUT";
133
134    @SuppressWarnings("deprecation")
135    private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY;
136
137    private Uri mLookupUri;
138    private String[] mExcludeMimes;
139    private int mExtraMode;
140    private int mStatusBarColor;
141    private boolean mHasAlreadyBeenOpened;
142
143    private ImageView mPhotoView;
144    private ExpandingEntryCardView mCommunicationCard;
145    private ExpandingEntryCardView mRecentCard;
146    private MultiShrinkScroller mScroller;
147    private SelectAccountDialogFragmentListener mSelectAccountFragmentListener;
148    private AsyncTask<Void, Void, Void> mEntriesAndActionsTask;
149
150    private static final int MIN_NUM_COMMUNICATION_ENTRIES_SHOWN = 3;
151    private static final int MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN = 3;
152
153    private Contact mContactData;
154    private ContactLoader mContactLoader;
155
156    private PorterDuffColorFilter mColorFilter;
157    List<Drawable> mDrawablesToTint;
158
159    private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter();
160
161    /**
162     * Keeps the default action per mimetype. Empty if no default actions are set
163     */
164    private HashMap<String, Action> mDefaultsMap = new HashMap<String, Action>();
165
166    /**
167     * Set of {@link Action} that are associated with the aggregate currently
168     * displayed by this dialog, represented as a map from {@link String}
169     * MIME-type to a list of {@link Action}.
170     */
171    private ActionMultiMap mActions = new ActionMultiMap();
172
173    /**
174     * {@link #LEADING_MIMETYPES} and {@link #TRAILING_MIMETYPES} are used to sort MIME-types.
175     *
176     * <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog,
177     * in the order specified here.</p>
178     *
179     * <p>The ones in {@link #TRAILING_MIMETYPES} appear in the end of the dialog, in the order
180     * specified here.</p>
181     *
182     * <p>The rest go between them, in the order in the array.</p>
183     */
184    private static final List<String> LEADING_MIMETYPES = Lists.newArrayList(
185            Phone.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE);
186
187    /** See {@link #LEADING_MIMETYPES}. */
188    private static final List<String> TRAILING_MIMETYPES = Lists.newArrayList(
189            StructuredPostal.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE);
190
191    /** Id for the background contact loader */
192    private static final int LOADER_CONTACT_ID = 0;
193
194    /** Id for the background Sms Loader */
195    private static final int LOADER_SMS_ID = 1;
196    private static final String KEY_LOADER_EXTRA_SMS_PHONES =
197            QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_SMS_PHONES";
198    private static final int MAX_SMS_RETRIEVE = 3;
199    private static final int LOADER_CALENDAR_ID = 2;
200    private static final String KEY_LOADER_EXTRA_CALENDAR_EMAILS =
201            QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_CALENDAR_EMAILS";
202    private static final int MAX_PAST_CALENDAR_RETRIEVE = 3;
203    private static final int MAX_FUTURE_CALENDAR_RETRIEVE = 3;
204    private static final long PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR =
205            180L * 24L * 60L * 60L * 1000L /* 180 days */;
206    private static final long FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR =
207            36L * 60L * 60L * 1000L /* 36 hours */;
208
209    private static final int[] mRecentLoaderIds = new int[]{LOADER_SMS_ID, LOADER_CALENDAR_ID};
210    private Map<Integer, List<ContactInteraction>> mRecentLoaderResults;
211
212    private static final String FRAGMENT_TAG_SELECT_ACCOUNT = "select_account_fragment";
213
214    final OnClickListener mEntryClickHandler = new OnClickListener() {
215        @Override
216        public void onClick(View v) {
217            Log.i(TAG, "mEntryClickHandler onClick");
218            Object intent = v.getTag();
219            if (intent == null || !(intent instanceof Intent)) {
220                return;
221            }
222            startActivity((Intent) intent);
223        }
224    };
225
226    /**
227     * Headless fragment used to handle account selection callbacks invoked from
228     * {@link DirectoryContactUtil}.
229     */
230    public static class SelectAccountDialogFragmentListener extends Fragment
231            implements SelectAccountDialogFragment.Listener {
232
233        private QuickContactActivity mQuickContactActivity;
234
235        public SelectAccountDialogFragmentListener() {}
236
237        @Override
238        public void onAccountChosen(AccountWithDataSet account, Bundle extraArgs) {
239            DirectoryContactUtil.createCopy(mQuickContactActivity.mContactData.getContentValues(),
240                    account, mQuickContactActivity);
241        }
242
243        @Override
244        public void onAccountSelectorCancelled() {}
245
246        /**
247         * Set the parent activity. Since rotation can cause this fragment to be used across
248         * more than one activity instance, we need to explicitly set this value instead
249         * of making this class non-static.
250         */
251        public void setQuickContactActivity(QuickContactActivity quickContactActivity) {
252            mQuickContactActivity = quickContactActivity;
253        }
254    }
255
256    final MultiShrinkScrollerListener mMultiShrinkScrollerListener
257            = new MultiShrinkScrollerListener() {
258        @Override
259        public void onScrolledOffBottom() {
260            onBackPressed();
261        }
262
263        @Override
264        public void onEnterFullscreen() {
265            updateStatusBarColor();
266        }
267
268        @Override
269        public void onExitFullscreen() {
270            updateStatusBarColor();
271        }
272    };
273
274    @Override
275    protected void onCreate(Bundle savedInstanceState) {
276        Trace.beginSection("onCreate()");
277        super.onCreate(savedInstanceState);
278
279        getWindow().setStatusBarColor(Color.TRANSPARENT);
280        // Since we can't disable Window animations from the Launcher, we can minimize the
281        // silliness of the animation by setting the navigation bar transparent.
282        getWindow().setNavigationBarColor(Color.TRANSPARENT);
283
284        processIntent(getIntent());
285
286        // Show QuickContact in front of soft input
287        getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
288                WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
289
290        setContentView(R.layout.quickcontact_activity);
291
292        mCommunicationCard = (ExpandingEntryCardView) findViewById(R.id.communication_card);
293        mRecentCard = (ExpandingEntryCardView) findViewById(R.id.recent_card);
294        mScroller = (MultiShrinkScroller) findViewById(R.id.multiscroller);
295
296        mCommunicationCard.setOnClickListener(mEntryClickHandler);
297        mCommunicationCard.setTitle(getResources().getString(R.string.communication_card_title));
298        mCommunicationCard.setExpandButtonText(
299        getResources().getString(R.string.expanding_entry_card_view_see_all));
300
301        mRecentCard.setOnClickListener(mEntryClickHandler);
302        mRecentCard.setTitle(getResources().getString(R.string.recent_card_title));
303
304        mPhotoView = (ImageView) findViewById(R.id.photo);
305
306        final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
307        setActionBar(toolbar);
308        setHeaderNameText(R.string.missing_name);
309
310        mHasAlreadyBeenOpened = savedInstanceState != null;
311
312        final ColorDrawable windowShim = new ColorDrawable(SHIM_COLOR);
313        getWindow().setBackgroundDrawable(windowShim);
314        if (!mHasAlreadyBeenOpened) {
315            final int duration = getResources().getInteger(android.R.integer.config_shortAnimTime);
316            ObjectAnimator.ofInt(windowShim, "alpha", 0, 0xFF).setDuration(duration).start();
317        }
318
319        if (mScroller != null) {
320            mScroller.initialize(mMultiShrinkScrollerListener);
321            if (mHasAlreadyBeenOpened) {
322                mScroller.setVisibility(View.VISIBLE);
323                mScroller.setScroll(mScroller.getScrollNeededToBeFullScreen());
324            } else {
325                // mScroller needs to perform asynchronous measurements after initalize(), therefore
326                // we can't mark this as GONE.
327                mScroller.setVisibility(View.INVISIBLE);
328            }
329        }
330
331        mDrawablesToTint = new ArrayList<>();
332        mSelectAccountFragmentListener= (SelectAccountDialogFragmentListener) getFragmentManager()
333                .findFragmentByTag(FRAGMENT_TAG_SELECT_ACCOUNT);
334        if (mSelectAccountFragmentListener == null) {
335            mSelectAccountFragmentListener = new SelectAccountDialogFragmentListener();
336            getFragmentManager().beginTransaction().add(0, mSelectAccountFragmentListener,
337                    FRAGMENT_TAG_SELECT_ACCOUNT).commit();
338            mSelectAccountFragmentListener.setRetainInstance(true);
339        }
340        mSelectAccountFragmentListener.setQuickContactActivity(this);
341
342        Trace.endSection();
343    }
344
345    protected void onActivityResult(int requestCode, int resultCode,
346            Intent data) {
347        if (requestCode == REQUEST_CODE_CONTACT_EDITOR_ACTIVITY &&
348                resultCode == ContactDeletionInteraction.RESULT_CODE_DELETED) {
349            // The contact that we were showing has been deleted.
350            finish();
351        }
352    }
353
354    @Override
355    protected void onNewIntent(Intent intent) {
356        super.onNewIntent(intent);
357        mHasAlreadyBeenOpened = true;
358        processIntent(intent);
359    }
360
361    private void processIntent(Intent intent) {
362        Uri lookupUri = intent.getData();
363
364        // Check to see whether it comes from the old version.
365        if (lookupUri != null && LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) {
366            final long rawContactId = ContentUris.parseId(lookupUri);
367            lookupUri = RawContacts.getContactLookupUri(getContentResolver(),
368                    ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
369        }
370        mExtraMode = getIntent().getIntExtra(QuickContact.EXTRA_MODE,
371                QuickContact.MODE_LARGE);
372        final Uri oldLookupUri = mLookupUri;
373
374        mLookupUri = Preconditions.checkNotNull(lookupUri, "missing lookupUri");
375        mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES);
376        if (oldLookupUri == null) {
377            mContactLoader = (ContactLoader) getLoaderManager().initLoader(
378                    LOADER_CONTACT_ID, null, mLoaderContactCallbacks);
379        } else if (oldLookupUri != mLookupUri) {
380            // After copying a directory contact, the contact URI changes. Therefore,
381            // we need to restart the loader and reload the new contact.
382            mContactLoader = (ContactLoader) getLoaderManager().restartLoader(
383                    LOADER_CONTACT_ID, null, mLoaderContactCallbacks);
384            for (int interactionLoaderId : mRecentLoaderIds) {
385                getLoaderManager().destroyLoader(interactionLoaderId);
386            }
387        }
388    }
389
390    private void runEntranceAnimation() {
391        if (mHasAlreadyBeenOpened) {
392            return;
393        }
394        mHasAlreadyBeenOpened = true;
395        final int bottomScroll = mScroller.getScrollUntilOffBottom() - 1;
396        final ObjectAnimator scrollAnimation
397                = ObjectAnimator.ofInt(mScroller, "scroll", -bottomScroll,
398                mExtraMode != MODE_FULLY_EXPANDED ? 0 : mScroller.getScrollNeededToBeFullScreen());
399        scrollAnimation.setDuration(ANIMATION_SLIDE_OPEN_DURATION);
400        scrollAnimation.start();
401    }
402
403    /** Assign this string to the view if it is not empty. */
404    private void setHeaderNameText(int resId) {
405        getActionBar().setTitle(getText(resId));
406    }
407
408    /** Assign this string to the view if it is not empty. */
409    private void setHeaderNameText(CharSequence value) {
410        if (!TextUtils.isEmpty(value)) {
411            getActionBar().setTitle(value);
412        }
413    }
414
415    /**
416     * Check if the given MIME-type appears in the list of excluded MIME-types
417     * that the most-recent caller requested.
418     */
419    private boolean isMimeExcluded(String mimeType) {
420        if (mExcludeMimes == null) return false;
421        for (String excludedMime : mExcludeMimes) {
422            if (TextUtils.equals(excludedMime, mimeType)) {
423                return true;
424            }
425        }
426        return false;
427    }
428
429    /**
430     * Handle the result from the ContactLoader
431     */
432    private void bindContactData(final Contact data) {
433        Trace.beginSection("bindContactData");
434        mContactData = data;
435        invalidateOptionsMenu();
436
437        mDefaultsMap.clear();
438
439        Trace.endSection();
440        Trace.beginSection("Set display photo & name");
441
442        mPhotoSetter.setupContactPhoto(data, mPhotoView);
443        extractAndApplyTintFromPhotoViewAsynchronously();
444        setHeaderNameText(data.getDisplayName());
445
446        Trace.endSection();
447
448        final List<String> sortedActionMimeTypes = Lists.newArrayList();
449        // Maintain a list of phone numbers to pass into SmsInteractionsLoader
450        final Set<String> phoneNumbers = new HashSet<>();
451        // Maintain a list of email addresses to pass into CalendarInteractionsLoader
452        final Set<String> emailAddresses = new HashSet<>();
453        // List of Entry that makes up the ExpandingEntryCardView
454        final List<Entry> entries = Lists.newArrayList();
455
456        mEntriesAndActionsTask = new AsyncTask<Void, Void, Void>() {
457            @Override
458            protected Void doInBackground(Void... params) {
459                computeEntriesAndActions(data, phoneNumbers, emailAddresses,
460                        sortedActionMimeTypes, entries);
461                return null;
462            }
463
464            @Override
465            protected void onPostExecute(Void aVoid) {
466                super.onPostExecute(aVoid);
467                // Check that original AsyncTask parameters are still valid and the activity
468                // is still running before binding to UI. A new intent could invalidate
469                // the results, for example.
470                if (data == mContactData && !isCancelled()) {
471                    bindEntriesAndActions(entries, phoneNumbers, emailAddresses,
472                            sortedActionMimeTypes);
473                    showActivity();
474                }
475            }
476        };
477        mEntriesAndActionsTask.execute();
478    }
479
480    private void bindEntriesAndActions(List<Entry> entries,
481            Set<String> phoneNumbers,
482            Set<String> emailAddresses,
483            List<String> sortedActionMimeTypes) {
484        Trace.beginSection("start sms loader");
485        final Bundle smsExtraBundle = new Bundle();
486        smsExtraBundle.putStringArray(KEY_LOADER_EXTRA_SMS_PHONES,
487                phoneNumbers.toArray(new String[phoneNumbers.size()]));
488        getLoaderManager().initLoader(
489                LOADER_SMS_ID,
490                smsExtraBundle,
491                mLoaderInteractionsCallbacks);
492        Trace.endSection();
493
494        Trace.beginSection("start calendar loader");
495        final Bundle calendarExtraBundle = new Bundle();
496        calendarExtraBundle.putStringArray(KEY_LOADER_EXTRA_CALENDAR_EMAILS,
497                emailAddresses.toArray(new String[emailAddresses.size()]));
498        getLoaderManager().initLoader(
499                LOADER_CALENDAR_ID,
500                calendarExtraBundle,
501                mLoaderInteractionsCallbacks);
502        Trace.endSection();
503
504        Trace.beginSection("bind communicate card");
505        if (entries.size() > 0) {
506            mCommunicationCard.initialize(entries,
507                    /* numInitialVisibleEntries = */ MIN_NUM_COMMUNICATION_ENTRIES_SHOWN,
508                    /* isExpanded = */ false,
509                    /* themeColor = */ 0);
510        }
511
512        final boolean hasData = !sortedActionMimeTypes.isEmpty();
513        mCommunicationCard.setVisibility(hasData ? View.VISIBLE : View.GONE);
514
515        Trace.endSection();
516    }
517
518    private void showActivity() {
519        if (mScroller != null) {
520            mScroller.setVisibility(View.VISIBLE);
521            SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false,
522                    new Runnable() {
523                        @Override
524                        public void run() {
525                            runEntranceAnimation();
526                        }
527                    });
528        }
529    }
530
531    private void computeEntriesAndActions(Contact data, Set<String> phoneNumbers,
532            Set<String> emailAddresses, List<String> sortedActionMimeTypes, List<Entry> entries) {
533        Trace.beginSection("inflate entries and actions");
534
535        final ResolveCache cache = ResolveCache.getInstance(this);
536        for (RawContact rawContact : data.getRawContacts()) {
537            for (DataItem dataItem : rawContact.getDataItems()) {
538                final String mimeType = dataItem.getMimeType();
539                final AccountType accountType = rawContact.getAccountType(this);
540                final DataKind dataKind = AccountTypeManager.getInstance(this)
541                        .getKindOrFallback(accountType, mimeType);
542
543                if (dataItem instanceof PhoneDataItem) {
544                    phoneNumbers.add(((PhoneDataItem) dataItem).getNormalizedNumber());
545                }
546
547                if (dataItem instanceof EmailDataItem) {
548                    emailAddresses.add(((EmailDataItem) dataItem).getAddress());
549                }
550
551                // Skip this data item if MIME-type excluded
552                if (isMimeExcluded(mimeType)) continue;
553
554                final long dataId = dataItem.getId();
555                final boolean isPrimary = dataItem.isPrimary();
556                final boolean isSuperPrimary = dataItem.isSuperPrimary();
557
558                if (dataKind != null) {
559                    // Build an action for this data entry, find a mapping to a UI
560                    // element, build its summary from the cursor, and collect it
561                    // along with all others of this MIME-type.
562                    final Action action = new DataAction(getApplicationContext(),
563                            dataItem, dataKind);
564                    final boolean wasAdded = considerAdd(action, cache, isSuperPrimary);
565                    if (wasAdded) {
566                        // Remember the default
567                        if (isSuperPrimary || (isPrimary && (mDefaultsMap.get(mimeType) == null))) {
568                            mDefaultsMap.put(mimeType, action);
569                        }
570                    }
571                }
572
573                // Handle Email rows with presence data as Im entry
574                final DataStatus status = data.getStatuses().get(dataId);
575                if (status != null && dataItem instanceof EmailDataItem) {
576                    final EmailDataItem email = (EmailDataItem) dataItem;
577                    final ImDataItem im = ImDataItem.createFromEmail(email);
578                    if (dataKind != null) {
579                        final DataAction action = new DataAction(getApplicationContext(),
580                                im, dataKind);
581                        action.setPresence(status.getPresence());
582                        considerAdd(action, cache, isSuperPrimary);
583                    }
584                }
585            }
586        }
587
588        Trace.endSection();
589        Trace.beginSection("collapsing action list");
590
591        // Collapse Action Lists (remove e.g. duplicate e-mail addresses from different sources)
592        for (List<Action> actionChildren : mActions.values()) {
593            Collapser.collapseList(actionChildren);
594        }
595
596        Trace.endSection();
597        Trace.beginSection("sort mimetypes");
598
599        // All the mime-types to add.
600        final Set<String> containedTypes = new HashSet<String>(mActions.keySet());
601        // First, add LEADING_MIMETYPES, which are most common.
602        for (String mimeType : LEADING_MIMETYPES) {
603            if (containedTypes.contains(mimeType)) {
604                sortedActionMimeTypes.add(mimeType);
605                containedTypes.remove(mimeType);
606                entries.addAll(actionsToEntries(mActions.get(mimeType)));
607            }
608        }
609
610        // Add all the remaining ones that are not TRAILING
611        for (String mimeType : containedTypes.toArray(new String[containedTypes.size()])) {
612            if (!TRAILING_MIMETYPES.contains(mimeType)) {
613                sortedActionMimeTypes.add(mimeType);
614                containedTypes.remove(mimeType);
615                entries.addAll(actionsToEntries(mActions.get(mimeType)));
616            }
617        }
618
619        // Then, add TRAILING_MIMETYPES, which are least common.
620        for (String mimeType : TRAILING_MIMETYPES) {
621            if (containedTypes.contains(mimeType)) {
622                containedTypes.remove(mimeType);
623                sortedActionMimeTypes.add(mimeType);
624                entries.addAll(actionsToEntries(mActions.get(mimeType)));
625            }
626        }
627
628        Trace.endSection();
629    }
630
631    /**
632     * Asynchronously extract the most vibrant color from the PhotoView. Once extracted,
633     * apply this tint to {@link MultiShrinkScroller}. This operation takes about 20-30ms
634     * on a Nexus 5.
635     */
636    private void extractAndApplyTintFromPhotoViewAsynchronously() {
637        if (mScroller == null) {
638            return;
639        }
640        final Drawable imageViewDrawable = mPhotoView.getDrawable();
641        new AsyncTask<Void, Void, Integer>() {
642            @Override
643            protected Integer doInBackground(Void... params) {
644                if (imageViewDrawable instanceof BitmapDrawable) {
645                    final Bitmap bitmap = ((BitmapDrawable) imageViewDrawable).getBitmap();
646                    return colorFromBitmap(bitmap);
647                }
648                if (imageViewDrawable instanceof LetterTileDrawable) {
649                    // LetterTileDrawable doesn't normally draw unless it is visible. Therefore,
650                    // we need to directly ask it for its color via getColor(). We could directly
651                    // return this color. However, in the future Palette#generate() may incorporate
652                    // saturation boosting. So I want to use Palette#generate() for the sake of
653                    // consistency.
654                    final LetterTileDrawable tileDrawable = (LetterTileDrawable) imageViewDrawable;
655                    final int PALETTE_BITMAP_SIZE = 1;
656                    final Bitmap bitmap = Bitmap.createBitmap(PALETTE_BITMAP_SIZE,
657                            PALETTE_BITMAP_SIZE, Bitmap.Config.ARGB_8888);
658                    // If Palette can not extract a primary color, our UX person says we are better
659                    // off using the LetterTileDrawable's non vibrant color than falling back
660                    // to the app's default color.
661                    final int color = colorFromBitmap(bitmap);
662                    if (color == 0) {
663                        return tileDrawable.getColor();
664                    } else {
665                        return color;
666                    }
667                }
668                return 0;
669            }
670
671            @Override
672            protected void onPostExecute(Integer color) {
673                super.onPostExecute(color);
674                mColorFilter = new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP);
675                // Make sure the color is valid. Also check that the Photo has not changed. If it
676                // has changed, the new tint color needs to be extracted
677                if (color != 0 && imageViewDrawable == mPhotoView.getDrawable()) {
678                    // TODO: animate from the previous tint.
679                    mScroller.setHeaderTintColor(color);
680
681                    // Create a darker version of the actionbar color. HSV is device dependent
682                    // and not perceptually-linear. Therefore, we can't say mStatusBarColor is
683                    // 70% as bright as the action bar color. We can only say: it is a bit darker.
684                    final float hsvComponents[] = new float[3];
685                    Color.colorToHSV(color, hsvComponents);
686                    hsvComponents[2] *= SYSTEM_BAR_BRIGHTNESS_FACTOR;
687                    mStatusBarColor = Color.HSVToColor(hsvComponents);
688
689                    updateStatusBarColor();
690                    for (Drawable drawable : mDrawablesToTint) {
691                        applyThemeColorIfAvailable(drawable);
692                    }
693                    mDrawablesToTint.clear();
694                }
695            }
696        }.execute();
697    }
698
699    private void updateStatusBarColor() {
700        if (mScroller == null) {
701            return;
702        }
703        final int desiredStatusBarColor;
704        // Only use a custom status bar color if QuickContacts touches the top of the viewport.
705        if (mScroller.getScrollNeededToBeFullScreen() <= 0) {
706            desiredStatusBarColor = mStatusBarColor;
707        } else {
708            desiredStatusBarColor = Color.TRANSPARENT;
709        }
710        // Animate to the new color.
711        if (desiredStatusBarColor != getWindow().getStatusBarColor()) {
712            final ObjectAnimator animation = ObjectAnimator.ofInt(getWindow(), "statusBarColor",
713                    getWindow().getStatusBarColor(), desiredStatusBarColor);
714            animation.setDuration(ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION);
715            animation.setEvaluator(new ArgbEvaluator());
716            animation.start();
717        }
718    }
719
720    private int colorFromBitmap(Bitmap bitmap) {
721        // Author of Palette recommends using 24 colors when analyzing profile photos.
722        final int NUMBER_OF_PALETTE_COLORS = 24;
723        final Palette palette = Palette.generate(bitmap, NUMBER_OF_PALETTE_COLORS);
724        if (palette != null && palette.getVibrantColor() != null) {
725            return palette.getVibrantColor().getRgb();
726        }
727        return 0;
728    }
729
730    /**
731     * Consider adding the given {@link Action}, which will only happen if
732     * {@link PackageManager} finds an application to handle
733     * {@link Action#getIntent()}.
734     * @param action the action to handle
735     * @param resolveCache cache of applications that can handle actions
736     * @param front indicates whether to add the action to the front of the list
737     * @return true if action has been added
738     */
739    private boolean considerAdd(Action action, ResolveCache resolveCache, boolean front) {
740        if (resolveCache.hasResolve(action)) {
741            mActions.put(action.getMimeType(), action, front);
742            return true;
743        }
744        return false;
745    }
746
747    /**
748     * Converts a list of Action into a list of Entry
749     * @param actions The list of Action to convert
750     * @return The converted list of Entry
751     */
752    private List<Entry> actionsToEntries(List<Action> actions) {
753        List<Entry> entries = new ArrayList<>();
754        for (Action action :  actions) {
755            String header = null;
756            String body = null;
757            String footer = null;
758            Drawable icon = null;
759            switch (action.getMimeType()) {
760                case Phone.CONTENT_ITEM_TYPE:
761                    header = String.valueOf(action.getBody());
762                    footer = String.valueOf(action.getSubtitle());
763                    icon = applyThemeColorIfAvailable(
764                            getResources().getDrawable(R.drawable.ic_phone_24dp));
765                    break;
766                case Email.CONTENT_ITEM_TYPE:
767                    header = String.valueOf(action.getBody());
768                    footer = String.valueOf(action.getSubtitle());
769                    icon = applyThemeColorIfAvailable(
770                            getResources().getDrawable(R.drawable.ic_email_24dp));
771                    break;
772                case StructuredPostal.CONTENT_ITEM_TYPE:
773                    header = String.valueOf(action.getBody());
774                    footer = String.valueOf(action.getSubtitle());
775                    icon = applyThemeColorIfAvailable(
776                            getResources().getDrawable(R.drawable.ic_place_24dp));
777                    break;
778                default:
779                    header = String.valueOf(action.getSubtitle());
780                    footer = String.valueOf(action.getBody());
781                    icon = ResolveCache.getInstance(this).getIcon(action);
782            }
783            entries.add(new Entry(icon, header, body, footer, action.getIntent(),
784                    /* isEditable= */ false));
785
786            // Add SMS in addition to phone calls
787            if (action.getMimeType().equals(Phone.CONTENT_ITEM_TYPE)) {
788                entries.add(new Entry(applyThemeColorIfAvailable(getResources().getDrawable(
789                        R.drawable.ic_message_24dp)),
790                        getResources().getString(R.string.send_message), null, header,
791                        action.getAlternateIntent(), /* isEditable = */ false));
792            }
793        }
794        return entries;
795    }
796
797    private List<Entry> contactInteractionsToEntries(List<ContactInteraction> interactions) {
798        List<Entry> entries = new ArrayList<>();
799        for (ContactInteraction interaction : interactions) {
800            entries.add(new Entry(applyThemeColorIfAvailable(interaction.getIcon(this)),
801                    interaction.getViewHeader(this),
802                    interaction.getViewBody(this),
803                    interaction.getBodyIcon(this),
804                    interaction.getViewFooter(this),
805                    interaction.getFooterIcon(this),
806                    interaction.getIntent(),
807                    /* isEditable = */ false));
808        }
809        return entries;
810    }
811
812    private LoaderCallbacks<Contact> mLoaderContactCallbacks =
813            new LoaderCallbacks<Contact>() {
814        @Override
815        public void onLoaderReset(Loader<Contact> loader) {
816        }
817
818        @Override
819        public void onLoadFinished(Loader<Contact> loader, Contact data) {
820            Trace.beginSection("onLoadFinished()");
821
822            if (isFinishing()) {
823                return;
824            }
825            if (data.isError()) {
826                // This shouldn't ever happen, so throw an exception. The {@link ContactLoader}
827                // should log the actual exception.
828                throw new IllegalStateException("Failed to load contact", data.getException());
829            }
830            if (data.isNotFound()) {
831                if (mHasAlreadyBeenOpened) {
832                    finish();
833                } else {
834                    Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri());
835                    Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage,
836                            Toast.LENGTH_LONG).show();
837                }
838                return;
839            }
840
841            bindContactData(data);
842
843            Trace.endSection();
844        }
845
846        @Override
847        public Loader<Contact> onCreateLoader(int id, Bundle args) {
848            if (mLookupUri == null) {
849                Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early");
850            }
851            // Load all contact data. We need loadGroupMetaData=true to determine whether the
852            // contact is invisible. If it is, we need to display an "Add to Contacts" MenuItem.
853            return new ContactLoader(getApplicationContext(), mLookupUri,
854                    true /*loadGroupMetaData*/, false /*loadInvitableAccountTypes*/,
855                    false /*postViewNotification*/, true /*computeFormattedPhoneNumber*/);
856        }
857    };
858
859    @Override
860    public void onBackPressed() {
861        if (mScroller != null) {
862            // TODO: implement exit animation if the scroller isn't already off the screen
863            finish();
864        } else {
865            super.onBackPressed();
866        }
867    }
868
869    @Override
870    public void finish() {
871        super.finish();
872
873        // override transitions to skip the standard window animations
874        overridePendingTransition(0, 0);
875    }
876
877    private LoaderCallbacks<List<ContactInteraction>> mLoaderInteractionsCallbacks =
878            new LoaderCallbacks<List<ContactInteraction>>() {
879
880        @Override
881        public Loader<List<ContactInteraction>> onCreateLoader(int id, Bundle args) {
882            Log.v(TAG, "onCreateLoader");
883            Loader<List<ContactInteraction>> loader = null;
884            switch (id) {
885                case LOADER_SMS_ID:
886                    Log.v(TAG, "LOADER_SMS_ID");
887                    loader = new SmsInteractionsLoader(
888                            QuickContactActivity.this,
889                            args.getStringArray(KEY_LOADER_EXTRA_SMS_PHONES),
890                            MAX_SMS_RETRIEVE);
891                    break;
892                case LOADER_CALENDAR_ID:
893                    Log.v(TAG, "LOADER_CALENDAR_ID");
894                    loader = new CalendarInteractionsLoader(
895                            QuickContactActivity.this,
896                            Arrays.asList(args.getStringArray(KEY_LOADER_EXTRA_CALENDAR_EMAILS)),
897                            MAX_FUTURE_CALENDAR_RETRIEVE,
898                            MAX_PAST_CALENDAR_RETRIEVE,
899                            FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR,
900                            PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR);
901                    break;
902            }
903            return loader;
904        }
905
906        @Override
907        public void onLoadFinished(Loader<List<ContactInteraction>> loader,
908                List<ContactInteraction> data) {
909            if (mRecentLoaderResults == null) {
910                mRecentLoaderResults = new HashMap<Integer, List<ContactInteraction>>();
911            }
912            Log.v(TAG, "onLoadFinished ~ loader.getId() " + loader.getId() + " data.size() " +
913                    data.size());
914            mRecentLoaderResults.put(loader.getId(), data);
915
916            if (isAllRecentDataLoaded()) {
917                bindRecentData();
918            }
919        }
920
921        @Override
922        public void onLoaderReset(Loader<List<ContactInteraction>> loader) {
923            mRecentLoaderResults.remove(loader.getId());
924        }
925
926    };
927
928    private boolean isAllRecentDataLoaded() {
929        return mRecentLoaderResults.size() == mRecentLoaderIds.length;
930    }
931
932    private void bindRecentData() {
933        List<ContactInteraction> allInteractions = new ArrayList<>();
934        for (List<ContactInteraction> loaderInteractions : mRecentLoaderResults.values()) {
935            allInteractions.addAll(loaderInteractions);
936        }
937
938        // Sort the interactions by most recent
939        Collections.sort(allInteractions, new Comparator<ContactInteraction>() {
940            @Override
941            public int compare(ContactInteraction a, ContactInteraction b) {
942                return a.getInteractionDate() >= b.getInteractionDate() ? -1 : 1;
943            }
944        });
945
946        if (allInteractions.size() > 0) {
947            mRecentCard.initialize(contactInteractionsToEntries(allInteractions),
948                    /* numInitialVisibleEntries = */ MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN,
949                    /* isExpanded = */ false,
950                    /* themeColor = */ 0);
951            mRecentCard.setVisibility(View.VISIBLE);
952        }
953    }
954
955    @Override
956    protected void onStop() {
957        super.onStop();
958
959        if (mEntriesAndActionsTask != null) {
960            // Once the activity is stopped, we will no longer want to bind mEntriesAndActionsTask's
961            // results on the UI thread. In some circumstances Activities are killed without
962            // onStop() being called. This is not a problem, because in these circumstances
963            // the entire process will be killed.
964            mEntriesAndActionsTask.cancel(/* mayInterruptIfRunning = */ false);
965        }
966    }
967
968    /**
969     * Applies the theme color as extracted in
970     * {@link #extractAndApplyTintFromPhotoViewAsynchronously()} if available. If the color is not
971     * available, store a reference to the drawable to tint when a color becomes available.
972     */
973    private Drawable applyThemeColorIfAvailable(Drawable drawable) {
974        if (mColorFilter != null) {
975            drawable.setColorFilter(mColorFilter);
976        } else {
977            mDrawablesToTint.add(drawable);
978        }
979        return drawable;
980    }
981
982    /**
983     * Returns true if it is possible to edit the current contact.
984     */
985    private boolean isContactEditable() {
986        return mContactData != null && !mContactData.isDirectoryEntry();
987    }
988
989    private void editContact() {
990        final Intent intent = new Intent(Intent.ACTION_EDIT, mLookupUri);
991        mContactLoader.cacheResult();
992        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
993        startActivityForResult(intent, REQUEST_CODE_CONTACT_EDITOR_ACTIVITY);
994    }
995
996    private void toggleStar(MenuItem starredMenuItem) {
997        // Make sure there is a contact
998        if (mLookupUri != null) {
999            // Read the current starred value from the UI instead of using the last
1000            // loaded state. This allows rapid tapping without writing the same
1001            // value several times
1002            final boolean isStarred = starredMenuItem.isChecked();
1003
1004            // To improve responsiveness, swap out the picture (and tag) in the UI already
1005            ContactDetailDisplayUtils.configureStarredMenuItem(starredMenuItem,
1006                    mContactData.isDirectoryEntry(), mContactData.isUserProfile(),
1007                    !isStarred);
1008
1009            // Now perform the real save
1010            Intent intent = ContactSaveService.createSetStarredIntent(
1011                    QuickContactActivity.this, mLookupUri, !isStarred);
1012            startService(intent);
1013        }
1014    }
1015
1016    /**
1017     * Calls into the contacts provider to get a pre-authorized version of the given URI.
1018     */
1019    private Uri getPreAuthorizedUri(Uri uri) {
1020        final Bundle uriBundle = new Bundle();
1021        uriBundle.putParcelable(ContactsContract.Authorization.KEY_URI_TO_AUTHORIZE, uri);
1022        final Bundle authResponse = getContentResolver().call(
1023                ContactsContract.AUTHORITY_URI,
1024                ContactsContract.Authorization.AUTHORIZATION_METHOD,
1025                null,
1026                uriBundle);
1027        if (authResponse != null) {
1028            return (Uri) authResponse.getParcelable(
1029                    ContactsContract.Authorization.KEY_AUTHORIZED_URI);
1030        } else {
1031            return uri;
1032        }
1033    }
1034    private void shareContact() {
1035        final String lookupKey = mContactData.getLookupKey();
1036        Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey);
1037        if (mContactData.isUserProfile()) {
1038            // User is sharing the profile.  We don't want to force the receiver to have
1039            // the highly-privileged READ_PROFILE permission, so we need to request a
1040            // pre-authorized URI from the provider.
1041            shareUri = getPreAuthorizedUri(shareUri);
1042        }
1043
1044        final Intent intent = new Intent(Intent.ACTION_SEND);
1045        intent.setType(Contacts.CONTENT_VCARD_TYPE);
1046        intent.putExtra(Intent.EXTRA_STREAM, shareUri);
1047
1048        // Launch chooser to share contact via
1049        final CharSequence chooseTitle = getText(R.string.share_via);
1050        final Intent chooseIntent = Intent.createChooser(intent, chooseTitle);
1051
1052        try {
1053            this.startActivity(chooseIntent);
1054        } catch (ActivityNotFoundException ex) {
1055            Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show();
1056        }
1057    }
1058
1059    /**
1060     * Creates a launcher shortcut with the current contact.
1061     */
1062    private void createLauncherShortcutWithContact() {
1063        final ShortcutIntentBuilder builder = new ShortcutIntentBuilder(this,
1064                new OnShortcutIntentCreatedListener() {
1065
1066                    @Override
1067                    public void onShortcutIntentCreated(Uri uri, Intent shortcutIntent) {
1068                        // Broadcast the shortcutIntent to the launcher to create a
1069                        // shortcut to this contact
1070                        shortcutIntent.setAction(ACTION_INSTALL_SHORTCUT);
1071                        QuickContactActivity.this.sendBroadcast(shortcutIntent);
1072
1073                        // Send a toast to give feedback to the user that a shortcut to this
1074                        // contact was added to the launcher.
1075                        Toast.makeText(QuickContactActivity.this,
1076                                R.string.createContactShortcutSuccessful,
1077                                Toast.LENGTH_SHORT).show();
1078                    }
1079
1080                });
1081        builder.createContactShortcutIntent(mLookupUri);
1082    }
1083
1084    @Override
1085    public boolean onCreateOptionsMenu(Menu menu) {
1086        MenuInflater inflater = getMenuInflater();
1087        inflater.inflate(R.menu.quickcontact, menu);
1088        return true;
1089    }
1090
1091    @Override
1092    public boolean onPrepareOptionsMenu(Menu menu) {
1093        if (mContactData != null) {
1094            final MenuItem starredMenuItem = menu.findItem(R.id.menu_star);
1095            ContactDetailDisplayUtils.configureStarredMenuItem(starredMenuItem,
1096                    mContactData.isDirectoryEntry(), mContactData.isUserProfile(),
1097                    mContactData.getStarred());
1098            // Configure edit MenuItem
1099            final MenuItem editMenuItem = menu.findItem(R.id.menu_edit);
1100            editMenuItem.setVisible(true);
1101            if (DirectoryContactUtil.isDirectoryContact(mContactData) || InvisibleContactUtil
1102                    .isInvisibleAndAddable(mContactData, this)) {
1103                editMenuItem.setIcon(R.drawable.ic_person_add_tinted_24dp);
1104            } else if (isContactEditable()) {
1105                editMenuItem.setIcon(R.drawable.ic_create_24dp);
1106            } else {
1107                editMenuItem.setVisible(false);
1108            }
1109            return true;
1110        }
1111        return false;
1112    }
1113
1114    @Override
1115    public boolean onOptionsItemSelected(MenuItem item) {
1116        switch (item.getItemId()) {
1117            case R.id.menu_star:
1118                toggleStar(item);
1119                return true;
1120            case R.id.menu_edit:
1121                if (DirectoryContactUtil.isDirectoryContact(mContactData)) {
1122                    DirectoryContactUtil.addToMyContacts(mContactData, this, getFragmentManager(),
1123                            mSelectAccountFragmentListener);
1124                } else if (InvisibleContactUtil.isInvisibleAndAddable(mContactData, this)) {
1125                    InvisibleContactUtil.addToDefaultGroup(mContactData, this);
1126                } else if (isContactEditable()) {
1127                    editContact();
1128                }
1129                return true;
1130            case R.id.menu_share:
1131                shareContact();
1132                return true;
1133            case R.id.menu_create_contact_shortcut:
1134                createLauncherShortcutWithContact();
1135                return true;
1136            default:
1137                return super.onOptionsItemSelected(item);
1138        }
1139    }
1140}
1141