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