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