QuickContactActivity.java revision cd321f65f1e50409812976380ad1f0fdb3fa35cb
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.app.Activity;
20import android.app.Fragment;
21import android.app.FragmentManager;
22import android.app.LoaderManager.LoaderCallbacks;
23import android.content.ActivityNotFoundException;
24import android.content.ContentUris;
25import android.content.Context;
26import android.content.Intent;
27import android.content.Loader;
28import android.content.pm.PackageManager;
29import android.graphics.Rect;
30import android.graphics.drawable.Drawable;
31import android.net.Uri;
32import android.os.Bundle;
33import android.os.Handler;
34import android.provider.ContactsContract.CommonDataKinds.Email;
35import android.provider.ContactsContract.CommonDataKinds.Phone;
36import android.provider.ContactsContract.CommonDataKinds.SipAddress;
37import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
38import android.provider.ContactsContract.CommonDataKinds.Website;
39import android.provider.ContactsContract.Contacts;
40import android.provider.ContactsContract.DisplayNameSources;
41import android.provider.ContactsContract.Intents.Insert;
42import android.provider.ContactsContract.Directory;
43import android.provider.ContactsContract.QuickContact;
44import android.provider.ContactsContract.RawContacts;
45import android.support.v13.app.FragmentPagerAdapter;
46import android.support.v4.view.PagerAdapter;
47import android.support.v4.view.ViewPager;
48import android.support.v4.view.ViewPager.SimpleOnPageChangeListener;
49import android.text.TextUtils;
50import android.util.Log;
51import android.view.MotionEvent;
52import android.view.View;
53import android.view.View.OnClickListener;
54import android.view.ViewGroup;
55import android.view.WindowManager;
56import android.widget.HorizontalScrollView;
57import android.widget.ImageView;
58import android.widget.RelativeLayout;
59import android.widget.TextView;
60import android.widget.Toast;
61
62import com.android.contacts.ContactSaveService;
63import com.android.contacts.common.Collapser;
64import com.android.contacts.R;
65import com.android.contacts.common.model.AccountTypeManager;
66import com.android.contacts.common.model.Contact;
67import com.android.contacts.common.model.ContactLoader;
68import com.android.contacts.common.model.RawContact;
69import com.android.contacts.common.model.account.AccountType;
70import com.android.contacts.common.model.dataitem.DataItem;
71import com.android.contacts.common.model.dataitem.DataKind;
72import com.android.contacts.common.model.dataitem.EmailDataItem;
73import com.android.contacts.common.model.dataitem.ImDataItem;
74import com.android.contacts.common.util.Constants;
75import com.android.contacts.common.util.DataStatus;
76import com.android.contacts.common.util.UriUtils;
77import com.android.contacts.util.ImageViewDrawableSetter;
78import com.android.contacts.util.SchedulingUtils;
79import com.android.contacts.common.util.StopWatch;
80import com.google.common.base.Preconditions;
81import com.google.common.collect.Lists;
82
83import java.util.HashMap;
84import java.util.HashSet;
85import java.util.List;
86import java.util.Set;
87
88// TODO: Save selected tab index during rotation
89
90/**
91 * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads
92 * data asynchronously, and then shows a popup with details centered around
93 * {@link Intent#getSourceBounds()}.
94 */
95public class QuickContactActivity extends Activity {
96    private static final String TAG = "QuickContact";
97
98    private static final boolean TRACE_LAUNCH = false;
99    private static final String TRACE_TAG = "quickcontact";
100    private static final int POST_DRAW_WAIT_DURATION = 60;
101    private static final boolean ENABLE_STOPWATCH = false;
102
103
104    @SuppressWarnings("deprecation")
105    private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY;
106
107    private Uri mLookupUri;
108    private String[] mExcludeMimes;
109    private List<String> mSortedActionMimeTypes = Lists.newArrayList();
110
111    private FloatingChildLayout mFloatingLayout;
112
113    private View mPhotoContainer;
114    private ViewGroup mTrack;
115    private HorizontalScrollView mTrackScroller;
116    private View mSelectedTabRectangle;
117    private View mLineAfterTrack;
118
119    private ImageView mPhotoView;
120    private ImageView mOpenDetailsOrAddContactImage;
121    private ImageView mStarImage;
122    private ViewPager mListPager;
123    private ViewPagerAdapter mPagerAdapter;
124
125    private Contact mContactData;
126    private ContactLoader mContactLoader;
127
128    private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter();
129
130    /**
131     * Keeps the default action per mimetype. Empty if no default actions are set
132     */
133    private HashMap<String, Action> mDefaultsMap = new HashMap<String, Action>();
134
135    /**
136     * Set of {@link Action} that are associated with the aggregate currently
137     * displayed by this dialog, represented as a map from {@link String}
138     * MIME-type to a list of {@link Action}.
139     */
140    private ActionMultiMap mActions = new ActionMultiMap();
141
142    /**
143     * {@link #LEADING_MIMETYPES} and {@link #TRAILING_MIMETYPES} are used to sort MIME-types.
144     *
145     * <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog,
146     * in the order specified here.</p>
147     *
148     * <p>The ones in {@link #TRAILING_MIMETYPES} appear in the end of the dialog, in the order
149     * specified here.</p>
150     *
151     * <p>The rest go between them, in the order in the array.</p>
152     */
153    private static final List<String> LEADING_MIMETYPES = Lists.newArrayList(
154            Phone.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE);
155
156    /** See {@link #LEADING_MIMETYPES}. */
157    private static final List<String> TRAILING_MIMETYPES = Lists.newArrayList(
158            StructuredPostal.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE);
159
160    /** Id for the background loader */
161    private static final int LOADER_ID = 0;
162
163    private StopWatch mStopWatch = ENABLE_STOPWATCH
164            ? StopWatch.start("QuickContact") : StopWatch.getNullStopWatch();
165
166    final OnClickListener mOpenDetailsClickHandler = new OnClickListener() {
167        @Override
168        public void onClick(View v) {
169            final Intent intent = new Intent(Intent.ACTION_VIEW, mLookupUri);
170            mContactLoader.cacheResult();
171            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
172            startActivity(intent);
173            close(false);
174        }
175    };
176
177    final OnClickListener mAddToContactsClickHandler = new OnClickListener() {
178        @Override
179        public void onClick(View v) {
180            if (mContactData == null) {
181                Log.e(TAG, "Empty contact data when trying to add to contact");
182                return;
183            }
184            final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
185            intent.setType(Contacts.CONTENT_ITEM_TYPE);
186
187            // Only pre-fill the name field if the provided display name is an organization
188            // name or better (e.g. structured name, nickname)
189            if (mContactData.getDisplayNameSource() >= DisplayNameSources.ORGANIZATION) {
190                intent.putExtra(Insert.NAME, mContactData.getDisplayName());
191            }
192            intent.putExtra(Insert.DATA, mContactData.getContentValues());
193            startActivity(intent);
194        }
195    };
196
197    @Override
198    protected void onCreate(Bundle icicle) {
199        mStopWatch.lap("c"); // create start
200        super.onCreate(icicle);
201
202        mStopWatch.lap("sc"); // super.onCreate
203
204        if (TRACE_LAUNCH) android.os.Debug.startMethodTracing(TRACE_TAG);
205
206        // Parse intent
207        final Intent intent = getIntent();
208
209        Uri lookupUri = intent.getData();
210
211        // Check to see whether it comes from the old version.
212        if (lookupUri != null && LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) {
213            final long rawContactId = ContentUris.parseId(lookupUri);
214            lookupUri = RawContacts.getContactLookupUri(getContentResolver(),
215                    ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
216        }
217
218        mLookupUri = Preconditions.checkNotNull(lookupUri, "missing lookupUri");
219
220        mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES);
221
222        mStopWatch.lap("i"); // intent parsed
223
224        mContactLoader = (ContactLoader) getLoaderManager().initLoader(
225                LOADER_ID, null, mLoaderCallbacks);
226
227        mStopWatch.lap("ld"); // loader started
228
229        // Show QuickContact in front of soft input
230        getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
231                WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
232
233        setContentView(R.layout.quickcontact_activity);
234
235        mStopWatch.lap("l"); // layout inflated
236
237        mFloatingLayout = (FloatingChildLayout) findViewById(R.id.floating_layout);
238        mTrack = (ViewGroup) findViewById(R.id.track);
239        mTrackScroller = (HorizontalScrollView) findViewById(R.id.track_scroller);
240        mOpenDetailsOrAddContactImage = (ImageView) findViewById(R.id.contact_details_image);
241        mStarImage = (ImageView) findViewById(R.id.quickcontact_star_button);
242        mListPager = (ViewPager) findViewById(R.id.item_list_pager);
243        mSelectedTabRectangle = findViewById(R.id.selected_tab_rectangle);
244        mLineAfterTrack = findViewById(R.id.line_after_track);
245
246        mFloatingLayout.setOnOutsideTouchListener(new View.OnTouchListener() {
247            @Override
248            public boolean onTouch(View v, MotionEvent event) {
249                handleOutsideTouch();
250                return true;
251            }
252        });
253
254        mOpenDetailsOrAddContactImage.setOnClickListener(mOpenDetailsClickHandler);
255
256        mPagerAdapter = new ViewPagerAdapter(getFragmentManager());
257        mListPager.setAdapter(mPagerAdapter);
258        mListPager.setOnPageChangeListener(new PageChangeListener());
259
260        final Rect sourceBounds = intent.getSourceBounds();
261        if (sourceBounds != null) {
262            mFloatingLayout.setChildTargetScreen(sourceBounds);
263        }
264
265        // find and prepare correct header view
266        mPhotoContainer = findViewById(R.id.photo_container);
267
268        setHeaderNameText(R.id.name, R.string.missing_name);
269
270        mPhotoView = (ImageView) mPhotoContainer.findViewById(R.id.photo);
271        mPhotoView.setOnClickListener(mOpenDetailsClickHandler);
272
273        mStopWatch.lap("v"); // view initialized
274
275        SchedulingUtils.doAfterLayout(mFloatingLayout, new Runnable() {
276            @Override
277            public void run() {
278                mFloatingLayout.fadeInBackground();
279            }
280        });
281
282        mStopWatch.lap("cf"); // onCreate finished
283    }
284
285    private void handleOutsideTouch() {
286        if (mFloatingLayout.isContentFullyVisible()) {
287            close(true);
288        }
289    }
290
291    private void close(boolean withAnimation) {
292        // cancel any pending queries
293        getLoaderManager().destroyLoader(LOADER_ID);
294
295        if (withAnimation) {
296            mFloatingLayout.fadeOutBackground();
297            final boolean animated = mFloatingLayout.hideContent(new Runnable() {
298                @Override
299                public void run() {
300                    // Wait until the final animation frame has been drawn, otherwise
301                    // there is jank as the framework transitions to the next Activity.
302                    SchedulingUtils.doAfterDraw(mFloatingLayout, new Runnable() {
303                        @Override
304                        public void run() {
305                            // Unfortunately, we need to also use postDelayed() to wait a moment
306                            // for the frame to be drawn, else the framework's activity-transition
307                            // animation will kick in before the final frame is available to it.
308                            // This seems unavoidable.  The problem isn't merely that there is no
309                            // post-draw listener API; if that were so, it would be sufficient to
310                            // call post() instead of postDelayed().
311                            new Handler().postDelayed(new Runnable() {
312                                @Override
313                                public void run() {
314                                    finish();
315                                }
316                            }, POST_DRAW_WAIT_DURATION);
317                        }
318                    });
319                }
320            });
321            if (!animated) {
322                // If we were in the wrong state, simply quit (this can happen for example
323                // if the user pushes BACK before anything has loaded)
324                finish();
325            }
326        } else {
327            finish();
328        }
329    }
330
331    @Override
332    public void onBackPressed() {
333        close(true);
334    }
335
336    /** Assign this string to the view if it is not empty. */
337    private void setHeaderNameText(int id, int resId) {
338        setHeaderNameText(id, getText(resId));
339    }
340
341    /** Assign this string to the view if it is not empty. */
342    private void setHeaderNameText(int id, CharSequence value) {
343        final View view = mPhotoContainer.findViewById(id);
344        if (view instanceof TextView) {
345            if (!TextUtils.isEmpty(value)) {
346                ((TextView)view).setText(value);
347            }
348        }
349    }
350
351    /**
352     * Check if the given MIME-type appears in the list of excluded MIME-types
353     * that the most-recent caller requested.
354     */
355    private boolean isMimeExcluded(String mimeType) {
356        if (mExcludeMimes == null) return false;
357        for (String excludedMime : mExcludeMimes) {
358            if (TextUtils.equals(excludedMime, mimeType)) {
359                return true;
360            }
361        }
362        return false;
363    }
364
365    /**
366     * Handle the result from the ContactLoader
367     */
368    private void bindData(Contact data) {
369        mContactData = data;
370        final ResolveCache cache = ResolveCache.getInstance(this);
371        final Context context = this;
372
373        mOpenDetailsOrAddContactImage.setVisibility(isMimeExcluded(Contacts.CONTENT_ITEM_TYPE) ?
374                View.GONE : View.VISIBLE);
375        final boolean isStarred = data.getStarred();
376        if (isStarred) {
377            mStarImage.setImageResource(R.drawable.ic_favorite_on_lt);
378            mStarImage.setContentDescription(
379                getResources().getString(R.string.menu_removeStar));
380        } else {
381            mStarImage.setImageResource(R.drawable.ic_favorite_off_lt);
382            mStarImage.setContentDescription(
383                getResources().getString(R.string.menu_addStar));
384        }
385        final Uri lookupUri = data.getLookupUri();
386
387        // If this is a json encoded URI, there is no local contact to star
388        if (UriUtils.isEncodedContactUri(lookupUri)) {
389            mStarImage.setVisibility(View.GONE);
390
391            // If directory export support is not allowed, then don't allow the user to add
392            // to contacts
393            if (mContactData.getDirectoryExportSupport() == Directory.EXPORT_SUPPORT_NONE) {
394                configureHeaderClickActions(false);
395            } else {
396                configureHeaderClickActions(true);
397            }
398        } else {
399            configureHeaderClickActions(false);
400            mStarImage.setVisibility(View.VISIBLE);
401            mStarImage.setOnClickListener(new OnClickListener() {
402                @Override
403                public void onClick(View view) {
404                    // Toggle "starred" state
405                    // Make sure there is a contact
406                    if (lookupUri != null) {
407                        // Changes the state of the image already before sending updates to the
408                        // database
409                        if (isStarred) {
410                            mStarImage.setImageResource(R.drawable.ic_favorite_off_lt);
411                        } else {
412                            mStarImage.setImageResource(R.drawable.ic_favorite_on_lt);
413                        }
414
415                        // Now perform the real save
416                        final Intent intent = ContactSaveService.createSetStarredIntent(context,
417                                lookupUri, !isStarred);
418                        context.startService(intent);
419                    }
420                }
421            });
422        }
423
424        mDefaultsMap.clear();
425
426        mStopWatch.lap("sph"); // Start photo setting
427
428        mPhotoSetter.setupContactPhoto(data, mPhotoView);
429
430        mStopWatch.lap("ph"); // Photo set
431
432        for (RawContact rawContact : data.getRawContacts()) {
433            for (DataItem dataItem : rawContact.getDataItems()) {
434                final String mimeType = dataItem.getMimeType();
435                final AccountType accountType = rawContact.getAccountType(this);
436                final DataKind dataKind = AccountTypeManager.getInstance(this)
437                        .getKindOrFallback(accountType, mimeType);
438
439                // Skip this data item if MIME-type excluded
440                if (isMimeExcluded(mimeType)) continue;
441
442                final long dataId = dataItem.getId();
443                final boolean isPrimary = dataItem.isPrimary();
444                final boolean isSuperPrimary = dataItem.isSuperPrimary();
445
446                if (dataKind != null) {
447                    // Build an action for this data entry, find a mapping to a UI
448                    // element, build its summary from the cursor, and collect it
449                    // along with all others of this MIME-type.
450                    final Action action = new DataAction(context, dataItem, dataKind);
451                    final boolean wasAdded = considerAdd(action, cache, isSuperPrimary);
452                    if (wasAdded) {
453                        // Remember the default
454                        if (isSuperPrimary || (isPrimary && (mDefaultsMap.get(mimeType) == null))) {
455                            mDefaultsMap.put(mimeType, action);
456                        }
457                    }
458                }
459
460                // Handle Email rows with presence data as Im entry
461                final DataStatus status = data.getStatuses().get(dataId);
462                if (status != null && dataItem instanceof EmailDataItem) {
463                    final EmailDataItem email = (EmailDataItem) dataItem;
464                    final ImDataItem im = ImDataItem.createFromEmail(email);
465                    if (dataKind != null) {
466                        final DataAction action = new DataAction(context, im, dataKind);
467                        action.setPresence(status.getPresence());
468                        considerAdd(action, cache, isSuperPrimary);
469                    }
470                }
471            }
472        }
473
474        mStopWatch.lap("e"); // Entities inflated
475
476        // Collapse Action Lists (remove e.g. duplicate e-mail addresses from different sources)
477        for (List<Action> actionChildren : mActions.values()) {
478            Collapser.collapseList(actionChildren);
479        }
480
481        mStopWatch.lap("c"); // List collapsed
482
483        setHeaderNameText(R.id.name, data.getDisplayName());
484
485        // All the mime-types to add.
486        final Set<String> containedTypes = new HashSet<String>(mActions.keySet());
487        mSortedActionMimeTypes.clear();
488        // First, add LEADING_MIMETYPES, which are most common.
489        for (String mimeType : LEADING_MIMETYPES) {
490            if (containedTypes.contains(mimeType)) {
491                mSortedActionMimeTypes.add(mimeType);
492                containedTypes.remove(mimeType);
493            }
494        }
495
496        // Add all the remaining ones that are not TRAILING
497        for (String mimeType : containedTypes.toArray(new String[containedTypes.size()])) {
498            if (!TRAILING_MIMETYPES.contains(mimeType)) {
499                mSortedActionMimeTypes.add(mimeType);
500                containedTypes.remove(mimeType);
501            }
502        }
503
504        // Then, add TRAILING_MIMETYPES, which are least common.
505        for (String mimeType : TRAILING_MIMETYPES) {
506            if (containedTypes.contains(mimeType)) {
507                containedTypes.remove(mimeType);
508                mSortedActionMimeTypes.add(mimeType);
509            }
510        }
511        mPagerAdapter.notifyDataSetChanged();
512
513        mStopWatch.lap("mt"); // Mime types initialized
514
515        // Add buttons for each mimetype
516        mTrack.removeAllViews();
517        for (String mimeType : mSortedActionMimeTypes) {
518            final View actionView = inflateAction(mimeType, cache, mTrack, data.getDisplayName());
519            mTrack.addView(actionView);
520        }
521
522        mStopWatch.lap("mt"); // Buttons added
523
524        final boolean hasData = !mSortedActionMimeTypes.isEmpty();
525        mTrackScroller.setVisibility(hasData ? View.VISIBLE : View.GONE);
526        mSelectedTabRectangle.setVisibility(hasData ? View.VISIBLE : View.GONE);
527        mLineAfterTrack.setVisibility(hasData ? View.VISIBLE : View.GONE);
528        mListPager.setVisibility(hasData ? View.VISIBLE : View.GONE);
529    }
530
531    /**
532     * Consider adding the given {@link Action}, which will only happen if
533     * {@link PackageManager} finds an application to handle
534     * {@link Action#getIntent()}.
535     * @param action the action to handle
536     * @param resolveCache cache of applications that can handle actions
537     * @param front indicates whether to add the action to the front of the list
538     * @return true if action has been added
539     */
540    private boolean considerAdd(Action action, ResolveCache resolveCache, boolean front) {
541        if (resolveCache.hasResolve(action)) {
542            mActions.put(action.getMimeType(), action, front);
543            return true;
544        }
545        return false;
546    }
547
548    /**
549     * Bind the correct image resource and click handlers to the header views
550     *
551     * @param canAdd Whether or not the user can directly add information in this quick contact
552     * to their local contacts
553     */
554    private void configureHeaderClickActions(boolean canAdd) {
555        if (canAdd) {
556            mOpenDetailsOrAddContactImage.setImageResource(R.drawable.ic_add_contact_holo_dark);
557            mOpenDetailsOrAddContactImage.setOnClickListener(mAddToContactsClickHandler);
558            mPhotoView.setOnClickListener(mAddToContactsClickHandler);
559        } else {
560            mOpenDetailsOrAddContactImage.setImageResource(R.drawable.ic_contacts_holo_dark);
561            mOpenDetailsOrAddContactImage.setOnClickListener(mOpenDetailsClickHandler);
562            mPhotoView.setOnClickListener(mOpenDetailsClickHandler);
563        }
564    }
565
566    /**
567     * Inflate the in-track view for the action of the given MIME-type, collapsing duplicate values.
568     * Will use the icon provided by the {@link DataKind}.
569     */
570    private View inflateAction(String mimeType, ResolveCache resolveCache,
571                               ViewGroup root, String name) {
572        final CheckableImageView typeView = (CheckableImageView) getLayoutInflater().inflate(
573                R.layout.quickcontact_track_button, root, false);
574
575        List<Action> children = mActions.get(mimeType);
576        typeView.setTag(mimeType);
577        final Action firstInfo = children.get(0);
578
579        // Set icon and listen for clicks
580        final CharSequence descrip = resolveCache.getDescription(firstInfo, name);
581        final Drawable icon = resolveCache.getIcon(firstInfo);
582        typeView.setChecked(false);
583        typeView.setContentDescription(descrip);
584        typeView.setImageDrawable(icon);
585        typeView.setOnClickListener(mTypeViewClickListener);
586
587        return typeView;
588    }
589
590    private CheckableImageView getActionViewAt(int position) {
591        return (CheckableImageView) mTrack.getChildAt(position);
592    }
593
594    @Override
595    public void onAttachFragment(Fragment fragment) {
596        final QuickContactListFragment listFragment = (QuickContactListFragment) fragment;
597        listFragment.setListener(mListFragmentListener);
598    }
599
600    private LoaderCallbacks<Contact> mLoaderCallbacks =
601            new LoaderCallbacks<Contact>() {
602        @Override
603        public void onLoaderReset(Loader<Contact> loader) {
604        }
605
606        @Override
607        public void onLoadFinished(Loader<Contact> loader, Contact data) {
608            mStopWatch.lap("lf"); // onLoadFinished
609            if (isFinishing()) {
610                close(false);
611                return;
612            }
613            if (data.isError()) {
614                // This shouldn't ever happen, so throw an exception. The {@link ContactLoader}
615                // should log the actual exception.
616                throw new IllegalStateException("Failed to load contact", data.getException());
617            }
618            if (data.isNotFound()) {
619                Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri());
620                Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage,
621                        Toast.LENGTH_LONG).show();
622                close(false);
623                return;
624            }
625
626            bindData(data);
627
628            mStopWatch.lap("bd"); // bindData finished
629
630            if (TRACE_LAUNCH) android.os.Debug.stopMethodTracing();
631            if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
632                Log.d(Constants.PERFORMANCE_TAG, "QuickContact shown");
633            }
634
635            // Data bound and ready, pull curtain to show. Put this on the Handler to ensure
636            // that the layout passes are completed
637            SchedulingUtils.doAfterLayout(mFloatingLayout, new Runnable() {
638                @Override
639                public void run() {
640                    mFloatingLayout.showContent(new Runnable() {
641                        @Override
642                        public void run() {
643                            mContactLoader.upgradeToFullContact();
644                        }
645                    });
646                }
647            });
648            mStopWatch.stopAndLog(TAG, 0);
649            mStopWatch = StopWatch.getNullStopWatch(); // We're done with it.
650        }
651
652        @Override
653        public Loader<Contact> onCreateLoader(int id, Bundle args) {
654            if (mLookupUri == null) {
655                Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early");
656            }
657            return new ContactLoader(getApplicationContext(), mLookupUri,
658                    false /*loadGroupMetaData*/, false /*loadInvitableAccountTypes*/,
659                    false /*postViewNotification*/, true /*computeFormattedPhoneNumber*/);
660        }
661    };
662
663    /** A type (e.g. Call/Addresses was clicked) */
664    private final OnClickListener mTypeViewClickListener = new OnClickListener() {
665        @Override
666        public void onClick(View view) {
667            final CheckableImageView actionView = (CheckableImageView)view;
668            final String mimeType = (String) actionView.getTag();
669            int index = mSortedActionMimeTypes.indexOf(mimeType);
670            mListPager.setCurrentItem(index, true);
671        }
672    };
673
674    private class ViewPagerAdapter extends FragmentPagerAdapter {
675        public ViewPagerAdapter(FragmentManager fragmentManager) {
676            super(fragmentManager);
677        }
678
679        @Override
680        public Fragment getItem(int position) {
681            final String mimeType = mSortedActionMimeTypes.get(position);
682            QuickContactListFragment fragment = new QuickContactListFragment(mimeType);
683            final List<Action> actions = mActions.get(mimeType);
684            fragment.setActions(actions);
685            return fragment;
686        }
687
688        @Override
689        public int getCount() {
690            return mSortedActionMimeTypes.size();
691        }
692
693        @Override
694        public int getItemPosition(Object object) {
695            final QuickContactListFragment fragment = (QuickContactListFragment) object;
696            final String mimeType = fragment.getMimeType();
697            for (int i = 0; i < mSortedActionMimeTypes.size(); i++) {
698                if (mimeType.equals(mSortedActionMimeTypes.get(i))) {
699                    return i;
700                }
701            }
702            return PagerAdapter.POSITION_NONE;
703        }
704    }
705
706    private class PageChangeListener extends SimpleOnPageChangeListener {
707        private int mScrollingState = ViewPager.SCROLL_STATE_IDLE;
708
709        @Override
710        public void onPageSelected(int position) {
711            final CheckableImageView actionView = getActionViewAt(position);
712            mTrackScroller.requestChildRectangleOnScreen(actionView,
713                    new Rect(0, 0, actionView.getWidth(), actionView.getHeight()), false);
714            // Don't render rectangle if we are currently scrolling to prevent it from flickering
715            if (mScrollingState == ViewPager.SCROLL_STATE_IDLE) {
716                renderSelectedRectangle(position, 0);
717            }
718        }
719
720        @Override
721        public void onPageScrollStateChanged(int state) {
722            super.onPageScrollStateChanged(state);
723            mScrollingState = state;
724        }
725
726        @Override
727        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
728            renderSelectedRectangle(position, positionOffset);
729        }
730
731        private void renderSelectedRectangle(int position, float positionOffset) {
732            final RelativeLayout.LayoutParams layoutParams =
733                    (RelativeLayout.LayoutParams) mSelectedTabRectangle.getLayoutParams();
734            final int width = layoutParams.width;
735            layoutParams.setMarginStart((int) ((position + positionOffset) * width));
736            mSelectedTabRectangle.setLayoutParams(layoutParams);
737        }
738    }
739
740    private final QuickContactListFragment.Listener mListFragmentListener =
741            new QuickContactListFragment.Listener() {
742        @Override
743        public void onOutsideClick() {
744            // If there is no background, we want to dismiss, because to the user it seems
745            // like he had touched outside. If the ViewPager is solid however, those taps
746            // must be ignored
747            final boolean isTransparent = mListPager.getBackground() == null;
748            if (isTransparent) handleOutsideTouch();
749        }
750
751        @Override
752        public void onItemClicked(final Action action, final boolean alternate) {
753            final Runnable startAppRunnable = new Runnable() {
754                @Override
755                public void run() {
756                    try {
757                        startActivity(alternate ? action.getAlternateIntent() : action.getIntent());
758                    } catch (ActivityNotFoundException e) {
759                        Toast.makeText(QuickContactActivity.this, R.string.quickcontact_missing_app,
760                                Toast.LENGTH_SHORT).show();
761                    }
762
763                    close(false);
764                }
765            };
766            // Defer the action to make the window properly repaint
767            new Handler().post(startAppRunnable);
768        }
769    };
770}
771