Home | History | Annotate | Download | only in quickcontact
      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 
     17 package com.android.contacts.quickcontact;
     18 
     19 import com.android.contacts.Collapser;
     20 import com.android.contacts.ContactLoader;
     21 import com.android.contacts.R;
     22 import com.android.contacts.model.AccountTypeManager;
     23 import com.android.contacts.model.DataKind;
     24 import com.android.contacts.util.Constants;
     25 import com.android.contacts.util.DataStatus;
     26 import com.android.contacts.util.ImageViewDrawableSetter;
     27 import com.android.contacts.util.SchedulingUtils;
     28 import com.android.contacts.util.StopWatch;
     29 import com.google.common.base.Preconditions;
     30 import com.google.common.collect.Lists;
     31 
     32 import android.app.Activity;
     33 import android.app.Fragment;
     34 import android.app.FragmentManager;
     35 import android.app.LoaderManager.LoaderCallbacks;
     36 import android.content.ActivityNotFoundException;
     37 import android.content.ContentUris;
     38 import android.content.ContentValues;
     39 import android.content.Context;
     40 import android.content.Entity;
     41 import android.content.Entity.NamedContentValues;
     42 import android.content.Intent;
     43 import android.content.Loader;
     44 import android.content.pm.PackageManager;
     45 import android.graphics.Rect;
     46 import android.graphics.drawable.Drawable;
     47 import android.net.Uri;
     48 import android.os.Bundle;
     49 import android.os.Handler;
     50 import android.provider.ContactsContract.CommonDataKinds.Email;
     51 import android.provider.ContactsContract.CommonDataKinds.Im;
     52 import android.provider.ContactsContract.CommonDataKinds.Phone;
     53 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
     54 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
     55 import android.provider.ContactsContract.CommonDataKinds.Website;
     56 import android.provider.ContactsContract.Contacts;
     57 import android.provider.ContactsContract.Data;
     58 import android.provider.ContactsContract.QuickContact;
     59 import android.provider.ContactsContract.RawContacts;
     60 import android.support.v13.app.FragmentPagerAdapter;
     61 import android.support.v4.view.ViewPager;
     62 import android.support.v4.view.ViewPager.SimpleOnPageChangeListener;
     63 import android.text.TextUtils;
     64 import android.util.Log;
     65 import android.view.MotionEvent;
     66 import android.view.View;
     67 import android.view.View.OnClickListener;
     68 import android.view.ViewGroup;
     69 import android.view.WindowManager;
     70 import android.widget.HorizontalScrollView;
     71 import android.widget.ImageButton;
     72 import android.widget.ImageView;
     73 import android.widget.RelativeLayout;
     74 import android.widget.TextView;
     75 import android.widget.Toast;
     76 
     77 import java.util.HashMap;
     78 import java.util.HashSet;
     79 import java.util.List;
     80 import 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  */
     89 public 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