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