QuickContactActivity.java revision 45ee872fe7be69c6b3f0c59167eecd11af467812
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 com.android.contacts.Collapser;
20import com.android.contacts.ContactLoader;
21import com.android.contacts.R;
22import com.android.contacts.model.AccountTypeManager;
23import com.android.contacts.model.DataKind;
24import com.android.contacts.util.Constants;
25import com.android.contacts.util.DataStatus;
26import com.android.contacts.util.ImageViewDrawableSetter;
27import com.android.contacts.util.SchedulingUtils;
28import com.android.contacts.util.StopWatch;
29import com.google.common.base.Preconditions;
30import com.google.common.collect.Lists;
31
32import android.app.Activity;
33import android.app.Fragment;
34import android.app.FragmentManager;
35import android.app.LoaderManager.LoaderCallbacks;
36import android.content.ActivityNotFoundException;
37import android.content.ContentUris;
38import android.content.ContentValues;
39import android.content.Context;
40import android.content.Entity;
41import android.content.Entity.NamedContentValues;
42import android.content.Intent;
43import android.content.Loader;
44import android.content.pm.PackageManager;
45import android.graphics.Rect;
46import android.graphics.drawable.Drawable;
47import android.net.Uri;
48import android.os.Bundle;
49import android.os.Handler;
50import android.provider.ContactsContract.CommonDataKinds.Email;
51import android.provider.ContactsContract.CommonDataKinds.Im;
52import android.provider.ContactsContract.CommonDataKinds.Phone;
53import android.provider.ContactsContract.CommonDataKinds.SipAddress;
54import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
55import android.provider.ContactsContract.CommonDataKinds.Website;
56import android.provider.ContactsContract.Contacts;
57import android.provider.ContactsContract.Data;
58import android.provider.ContactsContract.QuickContact;
59import android.provider.ContactsContract.RawContacts;
60import android.support.v13.app.FragmentPagerAdapter;
61import android.support.v4.view.ViewPager;
62import android.support.v4.view.ViewPager.SimpleOnPageChangeListener;
63import android.text.TextUtils;
64import android.util.Log;
65import android.view.MotionEvent;
66import android.view.View;
67import android.view.View.OnClickListener;
68import android.view.ViewGroup;
69import android.view.WindowManager;
70import android.widget.HorizontalScrollView;
71import android.widget.ImageButton;
72import android.widget.ImageView;
73import android.widget.RelativeLayout;
74import android.widget.TextView;
75import android.widget.Toast;
76
77import java.util.HashMap;
78import java.util.HashSet;
79import java.util.List;
80import java.util.Set;
81
82// TODO: Save selected tab index during rotation
83
84/**
85 * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads
86 * data asynchronously, and then shows a popup with details centered around
87 * {@link Intent#getSourceBounds()}.
88 */
89public class QuickContactActivity extends Activity {
90    private static final String TAG = "QuickContact";
91
92    private static final boolean TRACE_LAUNCH = false;
93    private static final String TRACE_TAG = "quickcontact";
94    private static final int POST_DRAW_WAIT_DURATION = 60;
95    private static final boolean ENABLE_STOPWATCH = false;
96
97
98    @SuppressWarnings("deprecation")
99    private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY;
100
101    private Uri mLookupUri;
102    private String[] mExcludeMimes;
103    private List<String> mSortedActionMimeTypes = Lists.newArrayList();
104
105    private FloatingChildLayout mFloatingLayout;
106
107    private View mPhotoContainer;
108    private ViewGroup mTrack;
109    private HorizontalScrollView mTrackScroller;
110    private View mSelectedTabRectangle;
111    private View mLineAfterTrack;
112
113    private ImageButton mOpenDetailsButton;
114    private ImageButton mOpenDetailsPushLayerButton;
115    private ViewPager mListPager;
116
117    private ContactLoader mContactLoader;
118
119    private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter();
120
121    /**
122     * Keeps the default action per mimetype. Empty if no default actions are set
123     */
124    private HashMap<String, Action> mDefaultsMap = new HashMap<String, Action>();
125
126    /**
127     * Set of {@link Action} that are associated with the aggregate currently
128     * displayed by this dialog, represented as a map from {@link String}
129     * MIME-type to a list of {@link Action}.
130     */
131    private ActionMultiMap mActions = new ActionMultiMap();
132
133    /**
134     * {@link #LEADING_MIMETYPES} and {@link #TRAILING_MIMETYPES} are used to sort MIME-types.
135     *
136     * <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog,
137     * in the order specified here.</p>
138     *
139     * <p>The ones in {@link #TRAILING_MIMETYPES} appear in the end of the dialog, in the order
140     * specified here.</p>
141     *
142     * <p>The rest go between them, in the order in the array.</p>
143     */
144    private static final List<String> LEADING_MIMETYPES = Lists.newArrayList(
145            Phone.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE);
146
147    /** See {@link #LEADING_MIMETYPES}. */
148    private static final List<String> TRAILING_MIMETYPES = Lists.newArrayList(
149            StructuredPostal.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE);
150
151    /** Id for the background loader */
152    private static final int LOADER_ID = 0;
153
154    private StopWatch mStopWatch = ENABLE_STOPWATCH
155            ? StopWatch.start("QuickContact") : StopWatch.getNullStopWatch();
156
157    @Override
158    protected void onCreate(Bundle icicle) {
159        mStopWatch.lap("c"); // create start
160        super.onCreate(icicle);
161
162        mStopWatch.lap("sc"); // super.onCreate
163
164        if (TRACE_LAUNCH) android.os.Debug.startMethodTracing(TRACE_TAG);
165
166        // Parse intent
167        final Intent intent = getIntent();
168
169        Uri lookupUri = intent.getData();
170
171        // Check to see whether it comes from the old version.
172        if (lookupUri != null && LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) {
173            final long rawContactId = ContentUris.parseId(lookupUri);
174            lookupUri = RawContacts.getContactLookupUri(getContentResolver(),
175                    ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
176        }
177
178        mLookupUri = Preconditions.checkNotNull(lookupUri, "missing lookupUri");
179
180        mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES);
181
182        mStopWatch.lap("i"); // intent parsed
183
184        mContactLoader = (ContactLoader) getLoaderManager().initLoader(
185                LOADER_ID, null, mLoaderCallbacks);
186
187        mStopWatch.lap("ld"); // loader started
188
189        // Show QuickContact in front of soft input
190        getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
191                WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
192
193        setContentView(R.layout.quickcontact_activity);
194
195        mStopWatch.lap("l"); // layout inflated
196
197        mFloatingLayout = (FloatingChildLayout) findViewById(R.id.floating_layout);
198        mTrack = (ViewGroup) findViewById(R.id.track);
199        mTrackScroller = (HorizontalScrollView) findViewById(R.id.track_scroller);
200        mOpenDetailsButton = (ImageButton) findViewById(R.id.open_details_button);
201        mOpenDetailsPushLayerButton = (ImageButton) findViewById(R.id.open_details_push_layer);
202        mListPager = (ViewPager) findViewById(R.id.item_list_pager);
203        mSelectedTabRectangle = findViewById(R.id.selected_tab_rectangle);
204        mLineAfterTrack = findViewById(R.id.line_after_track);
205
206        mFloatingLayout.setOnOutsideTouchListener(new View.OnTouchListener() {
207            @Override
208            public boolean onTouch(View v, MotionEvent event) {
209                handleOutsideTouch();
210                return true;
211            }
212        });
213
214        final OnClickListener openDetailsClickHandler = new OnClickListener() {
215            @Override
216            public void onClick(View v) {
217                final Intent intent = new Intent(Intent.ACTION_VIEW, mLookupUri);
218                mContactLoader.cacheResult();
219                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
220                startActivity(intent);
221                close(false);
222            }
223        };
224        mOpenDetailsButton.setOnClickListener(openDetailsClickHandler);
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(ContactLoader.Result data) {
334        final ResolveCache cache = ResolveCache.getInstance(this);
335        final Context context = this;
336
337        mOpenDetailsButton.setVisibility(isMimeExcluded(Contacts.CONTENT_ITEM_TYPE) ? View.GONE
338                : View.VISIBLE);
339
340        mDefaultsMap.clear();
341
342        mStopWatch.lap("atm"); // AccountTypeManager initialization start
343        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(
344                context.getApplicationContext());
345        mStopWatch.lap("fatm"); // AccountTypeManager initialization finished
346
347        final ImageView photoView = (ImageView) mPhotoContainer.findViewById(R.id.photo);
348        mPhotoSetter.setupContactPhoto(data, photoView);
349
350        mStopWatch.lap("ph"); // Photo set
351
352        for (Entity entity : data.getEntities()) {
353            final ContentValues entityValues = entity.getEntityValues();
354            final String accountType = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
355            final String dataSet = entityValues.getAsString(RawContacts.DATA_SET);
356            for (NamedContentValues subValue : entity.getSubValues()) {
357                final ContentValues entryValues = subValue.values;
358                final String mimeType = entryValues.getAsString(Data.MIMETYPE);
359
360                // Skip this data item if MIME-type excluded
361                if (isMimeExcluded(mimeType)) continue;
362
363                final long dataId = entryValues.getAsLong(Data._ID);
364                final Integer primary = entryValues.getAsInteger(Data.IS_PRIMARY);
365                final boolean isPrimary = primary != null && primary != 0;
366                final Integer superPrimary = entryValues.getAsInteger(Data.IS_SUPER_PRIMARY);
367                final boolean isSuperPrimary = superPrimary != null && superPrimary != 0;
368
369                final DataKind kind =
370                        accountTypes.getKindOrFallback(accountType, dataSet, mimeType);
371
372                if (kind != null) {
373                    // Build an action for this data entry, find a mapping to a UI
374                    // element, build its summary from the cursor, and collect it
375                    // along with all others of this MIME-type.
376                    final Action action = new DataAction(context, mimeType, kind, dataId,
377                            entryValues);
378                    final boolean wasAdded = considerAdd(action, cache, isSuperPrimary);
379                    if (wasAdded) {
380                        // Remember the default
381                        if (isSuperPrimary || (isPrimary && (mDefaultsMap.get(mimeType) == null))) {
382                            mDefaultsMap.put(mimeType, action);
383                        }
384                    }
385                }
386
387                // Handle Email rows with presence data as Im entry
388                final DataStatus status = data.getStatuses().get(dataId);
389                if (status != null && Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
390                    final DataKind imKind = accountTypes.getKindOrFallback(accountType, dataSet,
391                            Im.CONTENT_ITEM_TYPE);
392                    if (imKind != null) {
393                        final DataAction action = new DataAction(context, Im.CONTENT_ITEM_TYPE,
394                                imKind, dataId, entryValues);
395                        action.setPresence(status.getPresence());
396                        considerAdd(action, cache, isSuperPrimary);
397                    }
398                }
399            }
400        }
401
402        mStopWatch.lap("e"); // Entities inflated
403
404        // Collapse Action Lists (remove e.g. duplicate e-mail addresses from different sources)
405        for (List<Action> actionChildren : mActions.values()) {
406            Collapser.collapseList(actionChildren);
407        }
408
409        mStopWatch.lap("c"); // List collapsed
410
411        setHeaderNameText(R.id.name, data.getDisplayName());
412
413        // All the mime-types to add.
414        final Set<String> containedTypes = new HashSet<String>(mActions.keySet());
415        mSortedActionMimeTypes.clear();
416        // First, add LEADING_MIMETYPES, which are most common.
417        for (String mimeType : LEADING_MIMETYPES) {
418            if (containedTypes.contains(mimeType)) {
419                mSortedActionMimeTypes.add(mimeType);
420                containedTypes.remove(mimeType);
421            }
422        }
423
424        // Add all the remaining ones that are not TRAILING
425        for (String mimeType : containedTypes.toArray(new String[containedTypes.size()])) {
426            if (!TRAILING_MIMETYPES.contains(mimeType)) {
427                mSortedActionMimeTypes.add(mimeType);
428                containedTypes.remove(mimeType);
429            }
430        }
431
432        // Then, add TRAILING_MIMETYPES, which are least common.
433        for (String mimeType : TRAILING_MIMETYPES) {
434            if (containedTypes.contains(mimeType)) {
435                containedTypes.remove(mimeType);
436                mSortedActionMimeTypes.add(mimeType);
437            }
438        }
439
440        mStopWatch.lap("mt"); // Mime types initialized
441
442        // Add buttons for each mimetype
443        mTrack.removeAllViews();
444        for (String mimeType : mSortedActionMimeTypes) {
445            final View actionView = inflateAction(mimeType, cache, mTrack);
446            mTrack.addView(actionView);
447        }
448
449        mStopWatch.lap("mt"); // Buttons added
450
451        final boolean hasData = !mSortedActionMimeTypes.isEmpty();
452        mTrackScroller.setVisibility(hasData ? View.VISIBLE : View.GONE);
453        mSelectedTabRectangle.setVisibility(hasData ? View.VISIBLE : View.GONE);
454        mLineAfterTrack.setVisibility(hasData ? View.VISIBLE : View.GONE);
455        mListPager.setVisibility(hasData ? View.VISIBLE : View.GONE);
456    }
457
458    /**
459     * Consider adding the given {@link Action}, which will only happen if
460     * {@link PackageManager} finds an application to handle
461     * {@link Action#getIntent()}.
462     * @param action the action to handle
463     * @param resolveCache cache of applications that can handle actions
464     * @param front indicates whether to add the action to the front of the list
465     * @return true if action has been added
466     */
467    private boolean considerAdd(Action action, ResolveCache resolveCache, boolean front) {
468        if (resolveCache.hasResolve(action)) {
469            mActions.put(action.getMimeType(), action, front);
470            return true;
471        }
472        return false;
473    }
474
475    /**
476     * Inflate the in-track view for the action of the given MIME-type, collapsing duplicate values.
477     * Will use the icon provided by the {@link DataKind}.
478     */
479    private View inflateAction(String mimeType, ResolveCache resolveCache, ViewGroup root) {
480        final CheckableImageView typeView = (CheckableImageView) getLayoutInflater().inflate(
481                R.layout.quickcontact_track_button, root, false);
482
483        List<Action> children = mActions.get(mimeType);
484        typeView.setTag(mimeType);
485        final Action firstInfo = children.get(0);
486
487        // Set icon and listen for clicks
488        final CharSequence descrip = resolveCache.getDescription(firstInfo);
489        final Drawable icon = resolveCache.getIcon(firstInfo);
490        typeView.setChecked(false);
491        typeView.setContentDescription(descrip);
492        typeView.setImageDrawable(icon);
493        typeView.setOnClickListener(mTypeViewClickListener);
494
495        return typeView;
496    }
497
498    private CheckableImageView getActionViewAt(int position) {
499        return (CheckableImageView) mTrack.getChildAt(position);
500    }
501
502    @Override
503    public void onAttachFragment(Fragment fragment) {
504        final QuickContactListFragment listFragment = (QuickContactListFragment) fragment;
505        listFragment.setListener(mListFragmentListener);
506    }
507
508    private LoaderCallbacks<ContactLoader.Result> mLoaderCallbacks =
509            new LoaderCallbacks<ContactLoader.Result>() {
510        @Override
511        public void onLoaderReset(Loader<ContactLoader.Result> loader) {
512        }
513
514        @Override
515        public void onLoadFinished(Loader<ContactLoader.Result> loader, ContactLoader.Result data) {
516            mStopWatch.lap("lf"); // onLoadFinished
517            if (isFinishing()) {
518                close(false);
519                return;
520            }
521            if (data.isError()) {
522                // This shouldn't ever happen, so throw an exception. The {@link ContactLoader}
523                // should log the actual exception.
524                throw new IllegalStateException("Failed to load contact", data.getException());
525            }
526            if (data.isNotFound()) {
527                Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri());
528                Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage,
529                        Toast.LENGTH_LONG).show();
530                close(false);
531                return;
532            }
533
534            bindData(data);
535
536            mStopWatch.lap("bd"); // bindData finished
537
538            if (TRACE_LAUNCH) android.os.Debug.stopMethodTracing();
539            if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
540                Log.d(Constants.PERFORMANCE_TAG, "QuickContact shown");
541            }
542
543            // Data bound and ready, pull curtain to show. Put this on the Handler to ensure
544            // that the layout passes are completed
545            SchedulingUtils.doAfterLayout(mFloatingLayout, new Runnable() {
546                @Override
547                public void run() {
548                    mFloatingLayout.showContent(new Runnable() {
549                        @Override
550                        public void run() {
551                            mContactLoader.upgradeToFullContact();
552                        }
553                    });
554                }
555            });
556            mStopWatch.stopAndLog(TAG, 0);
557            mStopWatch = StopWatch.getNullStopWatch(); // We're done with it.
558        }
559
560        @Override
561        public Loader<ContactLoader.Result> onCreateLoader(int id, Bundle args) {
562            if (mLookupUri == null) {
563                Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early");
564            }
565            return new ContactLoader(getApplicationContext(), mLookupUri, false);
566        }
567    };
568
569    /** A type (e.g. Call/Addresses was clicked) */
570    private final OnClickListener mTypeViewClickListener = new OnClickListener() {
571        @Override
572        public void onClick(View view) {
573            final CheckableImageView actionView = (CheckableImageView)view;
574            final String mimeType = (String) actionView.getTag();
575            int index = mSortedActionMimeTypes.indexOf(mimeType);
576            mListPager.setCurrentItem(index, true);
577        }
578    };
579
580    private class ViewPagerAdapter extends FragmentPagerAdapter {
581        public ViewPagerAdapter(FragmentManager fragmentManager) {
582            super(fragmentManager);
583        }
584
585        @Override
586        public Fragment getItem(int position) {
587            QuickContactListFragment fragment = new QuickContactListFragment();
588            final String mimeType = mSortedActionMimeTypes.get(position);
589            final List<Action> actions = mActions.get(mimeType);
590            fragment.setActions(actions);
591            return fragment;
592        }
593
594        @Override
595        public int getCount() {
596            return mSortedActionMimeTypes.size();
597        }
598    }
599
600    private class PageChangeListener extends SimpleOnPageChangeListener {
601        @Override
602        public void onPageSelected(int position) {
603            final CheckableImageView actionView = getActionViewAt(position);
604            mTrackScroller.requestChildRectangleOnScreen(actionView,
605                    new Rect(0, 0, actionView.getWidth(), actionView.getHeight()), false);
606        }
607
608        @Override
609        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
610            final RelativeLayout.LayoutParams layoutParams =
611                    (RelativeLayout.LayoutParams) mSelectedTabRectangle.getLayoutParams();
612            final int width = mSelectedTabRectangle.getWidth();
613            layoutParams.leftMargin = (int) ((position + positionOffset) * width);
614            mSelectedTabRectangle.setLayoutParams(layoutParams);
615        }
616    }
617
618    private final QuickContactListFragment.Listener mListFragmentListener =
619            new QuickContactListFragment.Listener() {
620        @Override
621        public void onOutsideClick() {
622            // If there is no background, we want to dismiss, because to the user it seems
623            // like he had touched outside. If the ViewPager is solid however, those taps
624            // must be ignored
625            final boolean isTransparent = mListPager.getBackground() == null;
626            if (isTransparent) handleOutsideTouch();
627        }
628
629        @Override
630        public void onItemClicked(final Action action, final boolean alternate) {
631            final Runnable startAppRunnable = new Runnable() {
632                @Override
633                public void run() {
634                    try {
635                        startActivity(alternate ? action.getAlternateIntent() : action.getIntent());
636                    } catch (ActivityNotFoundException e) {
637                        Toast.makeText(QuickContactActivity.this, R.string.quickcontact_missing_app,
638                                Toast.LENGTH_SHORT).show();
639                    }
640
641                    close(false);
642                }
643            };
644            // Defer the action to make the window properly repaint
645            new Handler().post(startAppRunnable);
646        }
647    };
648}
649