• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
onCreate(Bundle icicle)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 
handleOutsideTouch()250     private void handleOutsideTouch() {
251         if (mFloatingLayout.isContentFullyVisible()) {
252             close(true);
253         }
254     }
255 
close(boolean withAnimation)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
onBackPressed()297     public void onBackPressed() {
298         close(true);
299     }
300 
301     /** Assign this string to the view if it is not empty. */
setHeaderNameText(int id, int resId)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. */
setHeaderNameText(int id, CharSequence value)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      */
isMimeExcluded(String mimeType)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      */
bindData(ContactLoader.Result data)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      */
considerAdd(Action action, ResolveCache resolveCache, boolean front)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      */
inflateAction(String mimeType, ResolveCache resolveCache, ViewGroup root)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 
getActionViewAt(int position)498     private CheckableImageView getActionViewAt(int position) {
499         return (CheckableImageView) mTrack.getChildAt(position);
500     }
501 
502     @Override
onAttachFragment(Fragment fragment)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 {
ViewPagerAdapter(FragmentManager fragmentManager)581         public ViewPagerAdapter(FragmentManager fragmentManager) {
582             super(fragmentManager);
583         }
584 
585         @Override
getItem(int position)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
getCount()595         public int getCount() {
596             return mSortedActionMimeTypes.size();
597         }
598     }
599 
600     private class PageChangeListener extends SimpleOnPageChangeListener {
601         @Override
onPageSelected(int position)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
onPageScrolled(int position, float positionOffset, int positionOffsetPixels)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