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.ContactPhotoManager;
     21 import com.android.contacts.ContactPresenceIconUtil;
     22 import com.android.contacts.R;
     23 import com.android.contacts.model.AccountTypeManager;
     24 import com.android.contacts.model.DataKind;
     25 import com.android.contacts.util.DataStatus;
     26 import com.android.contacts.util.NotifyingAsyncQueryHandler;
     27 import com.android.contacts.util.NotifyingAsyncQueryHandler.AsyncQueryListener;
     28 import com.google.common.base.Preconditions;
     29 import com.google.common.collect.Lists;
     30 
     31 import android.app.Activity;
     32 import android.app.Fragment;
     33 import android.app.FragmentManager;
     34 import android.content.ActivityNotFoundException;
     35 import android.content.ContentUris;
     36 import android.content.Context;
     37 import android.content.Intent;
     38 import android.content.pm.PackageManager;
     39 import android.content.res.AssetFileDescriptor;
     40 import android.database.Cursor;
     41 import android.graphics.Bitmap;
     42 import android.graphics.BitmapFactory;
     43 import android.graphics.Rect;
     44 import android.graphics.drawable.Drawable;
     45 import android.net.Uri;
     46 import android.os.AsyncTask;
     47 import android.os.Bundle;
     48 import android.os.Handler;
     49 import android.provider.ContactsContract.CommonDataKinds.Email;
     50 import android.provider.ContactsContract.CommonDataKinds.Im;
     51 import android.provider.ContactsContract.CommonDataKinds.Phone;
     52 import android.provider.ContactsContract.CommonDataKinds.Photo;
     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.DisplayPhoto;
     59 import android.provider.ContactsContract.QuickContact;
     60 import android.provider.ContactsContract.RawContacts;
     61 import android.support.v13.app.FragmentPagerAdapter;
     62 import android.support.v4.view.ViewPager;
     63 import android.support.v4.view.ViewPager.SimpleOnPageChangeListener;
     64 import android.text.TextUtils;
     65 import android.util.Log;
     66 import android.view.MotionEvent;
     67 import android.view.View;
     68 import android.view.View.OnClickListener;
     69 import android.view.ViewGroup;
     70 import android.view.WindowManager;
     71 import android.widget.HorizontalScrollView;
     72 import android.widget.ImageButton;
     73 import android.widget.ImageView;
     74 import android.widget.RelativeLayout;
     75 import android.widget.TextView;
     76 import android.widget.Toast;
     77 
     78 import java.io.IOException;
     79 import java.util.HashMap;
     80 import java.util.HashSet;
     81 import java.util.List;
     82 import java.util.Set;
     83 
     84 // TODO: Save selected tab index during rotation
     85 
     86 /**
     87  * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads
     88  * data asynchronously, and then shows a popup with details centered around
     89  * {@link Intent#getSourceBounds()}.
     90  */
     91 public class QuickContactActivity extends Activity {
     92     private static final String TAG = "QuickContact";
     93 
     94     private static final boolean TRACE_LAUNCH = false;
     95     private static final String TRACE_TAG = "quickcontact";
     96 
     97     @SuppressWarnings("deprecation")
     98     private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY;
     99 
    100     private NotifyingAsyncQueryHandler mHandler;
    101 
    102     private Uri mLookupUri;
    103     private String[] mExcludeMimes;
    104     private List<String> mSortedActionMimeTypes = Lists.newArrayList();
    105 
    106     private boolean mHasFinishedAnimatingIn = false;
    107     private boolean mHasStartedAnimatingOut = false;
    108 
    109     private FloatingChildLayout mFloatingLayout;
    110 
    111     private View mPhotoContainer;
    112     private ViewGroup mTrack;
    113     private HorizontalScrollView mTrackScroller;
    114     private View mSelectedTabRectangle;
    115     private View mLineAfterTrack;
    116 
    117     private ImageButton mOpenDetailsButton;
    118     private ImageButton mOpenDetailsPushLayerButton;
    119     private ViewPager mListPager;
    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 handler that loads the data */
    152     private static final int HANDLER_ID_DATA = 1;
    153 
    154     @Override
    155     protected void onCreate(Bundle icicle) {
    156         super.onCreate(icicle);
    157 
    158         // Show QuickContact in front of soft input
    159         getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
    160                 WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
    161 
    162         setContentView(R.layout.quickcontact_activity);
    163 
    164         mFloatingLayout = (FloatingChildLayout) findViewById(R.id.floating_layout);
    165         mTrack = (ViewGroup) findViewById(R.id.track);
    166         mTrackScroller = (HorizontalScrollView) findViewById(R.id.track_scroller);
    167         mOpenDetailsButton = (ImageButton) findViewById(R.id.open_details_button);
    168         mOpenDetailsPushLayerButton = (ImageButton) findViewById(R.id.open_details_push_layer);
    169         mListPager = (ViewPager) findViewById(R.id.item_list_pager);
    170         mSelectedTabRectangle = findViewById(R.id.selected_tab_rectangle);
    171         mLineAfterTrack = findViewById(R.id.line_after_track);
    172 
    173         mFloatingLayout.setOnOutsideTouchListener(new View.OnTouchListener() {
    174             @Override
    175             public boolean onTouch(View v, MotionEvent event) {
    176                 return handleOutsideTouch();
    177             }
    178         });
    179 
    180         final OnClickListener openDetailsClickHandler = new OnClickListener() {
    181             @Override
    182             public void onClick(View v) {
    183                 final Intent intent = new Intent(Intent.ACTION_VIEW, mLookupUri);
    184                 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
    185                 startActivity(intent);
    186                 hide(false);
    187             }
    188         };
    189         mOpenDetailsButton.setOnClickListener(openDetailsClickHandler);
    190         mOpenDetailsPushLayerButton.setOnClickListener(openDetailsClickHandler);
    191         mListPager.setAdapter(new ViewPagerAdapter(getFragmentManager()));
    192         mListPager.setOnPageChangeListener(new PageChangeListener());
    193 
    194         mHandler = new NotifyingAsyncQueryHandler(this, mQueryListener);
    195 
    196         show();
    197     }
    198 
    199     private void show() {
    200 
    201         if (TRACE_LAUNCH) {
    202             android.os.Debug.startMethodTracing(TRACE_TAG);
    203         }
    204 
    205         final Intent intent = getIntent();
    206 
    207         Uri lookupUri = intent.getData();
    208 
    209         // Check to see whether it comes from the old version.
    210         if (LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) {
    211             final long rawContactId = ContentUris.parseId(lookupUri);
    212             lookupUri = RawContacts.getContactLookupUri(getContentResolver(),
    213                     ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
    214         }
    215 
    216         mLookupUri = Preconditions.checkNotNull(lookupUri, "missing lookupUri");
    217 
    218         // Read requested parameters for displaying
    219         final Rect targetScreen = intent.getSourceBounds();
    220         Preconditions.checkNotNull(targetScreen, "missing targetScreen");
    221         mFloatingLayout.setChildTargetScreen(targetScreen);
    222 
    223         mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES);
    224 
    225         // find and prepare correct header view
    226         mPhotoContainer = findViewById(R.id.photo_container);
    227         setHeaderNameText(R.id.name, R.string.missing_name);
    228 
    229         // Start background query for data, but only select photo rows when they
    230         // directly match the super-primary PHOTO_ID.
    231         final Uri dataUri = Uri.withAppendedPath(lookupUri, Contacts.Data.CONTENT_DIRECTORY);
    232         mHandler.cancelOperation(HANDLER_ID_DATA);
    233 
    234         // Select all data items of the contact (except for photos, where we only select the display
    235         // photo)
    236         mHandler.startQuery(HANDLER_ID_DATA, lookupUri, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
    237                 + "!=? OR (" + Data.MIMETYPE + "=? AND " + Data._ID + "=" + Contacts.PHOTO_ID
    238                 + ")", new String[] { Photo.CONTENT_ITEM_TYPE, Photo.CONTENT_ITEM_TYPE }, null);
    239     }
    240 
    241     private boolean handleOutsideTouch() {
    242         if (!mHasFinishedAnimatingIn) return false;
    243         if (mHasStartedAnimatingOut) return false;
    244 
    245         mHasStartedAnimatingOut = true;
    246         hide(true);
    247         return true;
    248     }
    249 
    250     private void hide(boolean withAnimation) {
    251         // cancel any pending queries
    252         mHandler.cancelOperation(HANDLER_ID_DATA);
    253 
    254         if (withAnimation) {
    255             mFloatingLayout.hideChild(new Runnable() {
    256                 @Override
    257                 public void run() {
    258                     finish();
    259                 }
    260             });
    261         } else {
    262             mFloatingLayout.hideChild(null);
    263             finish();
    264         }
    265     }
    266 
    267     @Override
    268     public void onBackPressed() {
    269         hide(true);
    270     }
    271 
    272     private final AsyncQueryListener mQueryListener = new AsyncQueryListener() {
    273         @Override
    274         public synchronized void onQueryComplete(int token, Object cookie, Cursor cursor) {
    275             try {
    276                 if (isFinishing()) {
    277                     hide(false);
    278                     return;
    279                 } else if (cursor == null || cursor.getCount() == 0) {
    280                     Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage,
    281                             Toast.LENGTH_LONG).show();
    282                     hide(false);
    283                     return;
    284                 }
    285 
    286                 bindData(cursor);
    287 
    288                 if (TRACE_LAUNCH) {
    289                     android.os.Debug.stopMethodTracing();
    290                 }
    291 
    292                 // Data bound and ready, pull curtain to show. Put this on the Handler to ensure
    293                 // that the layout passes are completed
    294                 mHandler.post(new Runnable() {
    295                     @Override
    296                     public void run() {
    297                         mFloatingLayout.showChild(new Runnable() {
    298                             @Override
    299                             public void run() {
    300                                 mHasFinishedAnimatingIn = true;
    301                             }
    302                         });
    303                     }
    304                 });
    305             } finally {
    306                 if (cursor != null) {
    307                     cursor.close();
    308                 }
    309             }
    310         }
    311     };
    312 
    313     /** Assign this string to the view if it is not empty. */
    314     private void setHeaderNameText(int id, int resId) {
    315         setHeaderNameText(id, getText(resId));
    316     }
    317 
    318     /** Assign this string to the view if it is not empty. */
    319     private void setHeaderNameText(int id, CharSequence value) {
    320         final View view = mPhotoContainer.findViewById(id);
    321         if (view instanceof TextView) {
    322             if (!TextUtils.isEmpty(value)) {
    323                 ((TextView)view).setText(value);
    324             }
    325         }
    326     }
    327 
    328     /**
    329      * Assign this string to the view (if found in {@link #mPhotoContainer}), or hiding this view
    330      * if there is no string.
    331      */
    332     private void setHeaderText(int id, int resId) {
    333         setHeaderText(id, getText(resId));
    334     }
    335 
    336     /**
    337      * Assign this string to the view (if found in {@link #mPhotoContainer}), or hiding this view
    338      * if there is no string.
    339      */
    340     private void setHeaderText(int id, CharSequence value) {
    341         final View view = mPhotoContainer.findViewById(id);
    342         if (view instanceof TextView) {
    343             ((TextView)view).setText(value);
    344             view.setVisibility(TextUtils.isEmpty(value) ? View.GONE : View.VISIBLE);
    345         }
    346     }
    347 
    348     /** Assign this image to the view, if found in {@link #mPhotoContainer}. */
    349     private void setHeaderImage(int id, Drawable drawable) {
    350         final View view = mPhotoContainer.findViewById(id);
    351         if (view instanceof ImageView) {
    352             ((ImageView)view).setImageDrawable(drawable);
    353             view.setVisibility(drawable == null ? View.GONE : View.VISIBLE);
    354         }
    355     }
    356 
    357     /**
    358      * Check if the given MIME-type appears in the list of excluded MIME-types
    359      * that the most-recent caller requested.
    360      */
    361     private boolean isMimeExcluded(String mimeType) {
    362         if (mExcludeMimes == null) return false;
    363         for (String excludedMime : mExcludeMimes) {
    364             if (TextUtils.equals(excludedMime, mimeType)) {
    365                 return true;
    366             }
    367         }
    368         return false;
    369     }
    370 
    371     /**
    372      * Handle the result from the {@link #TOKEN_DATA} query.
    373      */
    374     private void bindData(Cursor cursor) {
    375         final ResolveCache cache = ResolveCache.getInstance(this);
    376         final Context context = this;
    377 
    378         mOpenDetailsButton.setVisibility(isMimeExcluded(Contacts.CONTENT_ITEM_TYPE) ? View.GONE
    379                 : View.VISIBLE);
    380 
    381         mDefaultsMap.clear();
    382 
    383         final DataStatus status = new DataStatus();
    384         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(
    385                 context.getApplicationContext());
    386         final ImageView photoView = (ImageView) mPhotoContainer.findViewById(R.id.photo);
    387 
    388         Bitmap photoBitmap = null;
    389         while (cursor.moveToNext()) {
    390             // Handle any social status updates from this row
    391             status.possibleUpdate(cursor);
    392 
    393             final String mimeType = cursor.getString(DataQuery.MIMETYPE);
    394 
    395             // Skip this data item if MIME-type excluded
    396             if (isMimeExcluded(mimeType)) continue;
    397 
    398             final long dataId = cursor.getLong(DataQuery._ID);
    399             final String accountType = cursor.getString(DataQuery.ACCOUNT_TYPE);
    400             final String dataSet = cursor.getString(DataQuery.DATA_SET);
    401             final boolean isPrimary = cursor.getInt(DataQuery.IS_PRIMARY) != 0;
    402             final boolean isSuperPrimary = cursor.getInt(DataQuery.IS_SUPER_PRIMARY) != 0;
    403 
    404             // Handle photos included as data row
    405             if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
    406                 final int displayPhotoColumnIndex = cursor.getColumnIndex(Photo.PHOTO_FILE_ID);
    407                 final boolean hasDisplayPhoto = !cursor.isNull(displayPhotoColumnIndex);
    408                 if (hasDisplayPhoto) {
    409                     final long displayPhotoId = cursor.getLong(displayPhotoColumnIndex);
    410                     final Uri displayPhotoUri = ContentUris.withAppendedId(
    411                             DisplayPhoto.CONTENT_URI, displayPhotoId);
    412                     // Fetch and JPEG uncompress on the background thread
    413                     new AsyncTask<Void, Void, Bitmap>() {
    414                         @Override
    415                         protected Bitmap doInBackground(Void... params) {
    416                             try {
    417                                 AssetFileDescriptor fd = getContentResolver()
    418                                         .openAssetFileDescriptor(displayPhotoUri, "r");
    419                                 return BitmapFactory.decodeStream(fd.createInputStream());
    420                             } catch (IOException e) {
    421                                 Log.e(TAG, "Error getting display photo. Ignoring, as we already " +
    422                                         "have the thumbnail", e);
    423                                 return null;
    424                             }
    425                         }
    426 
    427                         @Override
    428                         protected void onPostExecute(Bitmap result) {
    429                             if (result == null) return;
    430                             photoView.setImageBitmap(result);
    431                         }
    432                     }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
    433                 }
    434                 final int photoColumnIndex = cursor.getColumnIndex(Photo.PHOTO);
    435                 final byte[] photoBlob = cursor.getBlob(photoColumnIndex);
    436                 if (photoBlob != null) {
    437                     photoBitmap = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
    438                 }
    439                 continue;
    440             }
    441 
    442             final DataKind kind = accountTypes.getKindOrFallback(accountType, dataSet, mimeType);
    443 
    444             if (kind != null) {
    445                 // Build an action for this data entry, find a mapping to a UI
    446                 // element, build its summary from the cursor, and collect it
    447                 // along with all others of this MIME-type.
    448                 final Action action = new DataAction(context, mimeType, kind, dataId, cursor);
    449                 final boolean wasAdded = considerAdd(action, cache);
    450                 if (wasAdded) {
    451                     // Remember the default
    452                     if (isSuperPrimary || (isPrimary && (mDefaultsMap.get(mimeType) == null))) {
    453                         mDefaultsMap.put(mimeType, action);
    454                     }
    455                 }
    456             }
    457 
    458             // Handle Email rows with presence data as Im entry
    459             final boolean hasPresence = !cursor.isNull(DataQuery.PRESENCE);
    460             if (hasPresence && Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
    461                 final DataKind imKind = accountTypes.getKindOrFallback(accountType, dataSet,
    462                         Im.CONTENT_ITEM_TYPE);
    463                 if (imKind != null) {
    464                     final DataAction action = new DataAction(context, Im.CONTENT_ITEM_TYPE, imKind,
    465                             dataId, cursor);
    466                     considerAdd(action, cache);
    467                 }
    468             }
    469         }
    470 
    471         // Collapse Action Lists (remove e.g. duplicate e-mail addresses from different sources)
    472         for (List<Action> actionChildren : mActions.values()) {
    473             Collapser.collapseList(actionChildren);
    474         }
    475 
    476         if (cursor.moveToLast()) {
    477             // Read contact name from last data row
    478             final String name = cursor.getString(DataQuery.DISPLAY_NAME);
    479             setHeaderNameText(R.id.name, name);
    480         }
    481 
    482         if (photoView != null) {
    483             // Place photo when discovered in data, otherwise show generic avatar
    484             if (photoBitmap != null) {
    485                 photoView.setImageBitmap(photoBitmap);
    486             } else {
    487                 photoView.setImageResource(ContactPhotoManager.getDefaultAvatarResId(true, false));
    488             }
    489         }
    490 
    491         // All the mime-types to add.
    492         final Set<String> containedTypes = new HashSet<String>(mActions.keySet());
    493         mSortedActionMimeTypes.clear();
    494         // First, add LEADING_MIMETYPES, which are most common.
    495         for (String mimeType : LEADING_MIMETYPES) {
    496             if (containedTypes.contains(mimeType)) {
    497                 mSortedActionMimeTypes.add(mimeType);
    498                 containedTypes.remove(mimeType);
    499             }
    500         }
    501 
    502         // Add all the remaining ones that are not TRAILING
    503         for (String mimeType : containedTypes.toArray(new String[containedTypes.size()])) {
    504             if (!TRAILING_MIMETYPES.contains(mimeType)) {
    505                 mSortedActionMimeTypes.add(mimeType);
    506                 containedTypes.remove(mimeType);
    507             }
    508         }
    509 
    510         // Then, add TRAILING_MIMETYPES, which are least common.
    511         for (String mimeType : TRAILING_MIMETYPES) {
    512             if (containedTypes.contains(mimeType)) {
    513                 containedTypes.remove(mimeType);
    514                 mSortedActionMimeTypes.add(mimeType);
    515             }
    516         }
    517 
    518         // Add buttons for each mimetype
    519         for (String mimeType : mSortedActionMimeTypes) {
    520             final View actionView = inflateAction(mimeType, cache, mTrack);
    521             mTrack.addView(actionView);
    522         }
    523 
    524         final boolean hasData = !mSortedActionMimeTypes.isEmpty();
    525         mTrackScroller.setVisibility(hasData ? View.VISIBLE : View.GONE);
    526         mSelectedTabRectangle.setVisibility(hasData ? View.VISIBLE : View.GONE);
    527         mLineAfterTrack.setVisibility(hasData ? View.VISIBLE : View.GONE);
    528         mListPager.setVisibility(hasData ? View.VISIBLE : View.GONE);
    529     }
    530 
    531     /**
    532      * Consider adding the given {@link Action}, which will only happen if
    533      * {@link PackageManager} finds an application to handle
    534      * {@link Action#getIntent()}.
    535      * @return true if action has been added
    536      */
    537     private boolean considerAdd(Action action, ResolveCache resolveCache) {
    538         if (resolveCache.hasResolve(action)) {
    539             mActions.put(action.getMimeType(), action);
    540             return true;
    541         }
    542         return false;
    543     }
    544 
    545     /**
    546      * Inflate the in-track view for the action of the given MIME-type, collapsing duplicate values.
    547      * Will use the icon provided by the {@link DataKind}.
    548      */
    549     private View inflateAction(String mimeType, ResolveCache resolveCache, ViewGroup root) {
    550         final CheckableImageView typeView = (CheckableImageView) getLayoutInflater().inflate(
    551                 R.layout.quickcontact_track_button, root, false);
    552 
    553         List<Action> children = mActions.get(mimeType);
    554         typeView.setTag(mimeType);
    555         final Action firstInfo = children.get(0);
    556 
    557         // Set icon and listen for clicks
    558         final CharSequence descrip = resolveCache.getDescription(firstInfo);
    559         final Drawable icon = resolveCache.getIcon(firstInfo);
    560         typeView.setChecked(false);
    561         typeView.setContentDescription(descrip);
    562         typeView.setImageDrawable(icon);
    563         typeView.setOnClickListener(mTypeViewClickListener);
    564 
    565         return typeView;
    566     }
    567 
    568     private CheckableImageView getActionViewAt(int position) {
    569         return (CheckableImageView) mTrack.getChildAt(position);
    570     }
    571 
    572     @Override
    573     public void onAttachFragment(Fragment fragment) {
    574         final QuickContactListFragment listFragment = (QuickContactListFragment) fragment;
    575         listFragment.setListener(mListFragmentListener);
    576     }
    577 
    578     /** A type (e.g. Call/Addresses was clicked) */
    579     private final OnClickListener mTypeViewClickListener = new OnClickListener() {
    580         @Override
    581         public void onClick(View view) {
    582             final CheckableImageView actionView = (CheckableImageView)view;
    583             final String mimeType = (String) actionView.getTag();
    584             int index = mSortedActionMimeTypes.indexOf(mimeType);
    585             mListPager.setCurrentItem(index, true);
    586         }
    587     };
    588 
    589     private class ViewPagerAdapter extends FragmentPagerAdapter {
    590         public ViewPagerAdapter(FragmentManager fragmentManager) {
    591             super(fragmentManager);
    592         }
    593 
    594         @Override
    595         public Fragment getItem(int position) {
    596             QuickContactListFragment fragment = new QuickContactListFragment();
    597             final String mimeType = mSortedActionMimeTypes.get(position);
    598             final List<Action> actions = mActions.get(mimeType);
    599             fragment.setActions(actions);
    600             return fragment;
    601         }
    602 
    603         @Override
    604         public int getCount() {
    605             return mSortedActionMimeTypes.size();
    606         }
    607     }
    608 
    609     private class PageChangeListener extends SimpleOnPageChangeListener {
    610         @Override
    611         public void onPageSelected(int position) {
    612             final CheckableImageView actionView = getActionViewAt(position);
    613             mTrackScroller.requestChildRectangleOnScreen(actionView,
    614                     new Rect(0, 0, actionView.getWidth(), actionView.getHeight()), false);
    615         }
    616 
    617         @Override
    618         public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
    619             final RelativeLayout.LayoutParams layoutParams =
    620                     (RelativeLayout.LayoutParams) mSelectedTabRectangle.getLayoutParams();
    621             final int width = mSelectedTabRectangle.getWidth();
    622             layoutParams.leftMargin = (int) ((position + positionOffset) * width);
    623             mSelectedTabRectangle.setLayoutParams(layoutParams);
    624         }
    625     }
    626 
    627     private final QuickContactListFragment.Listener mListFragmentListener =
    628             new QuickContactListFragment.Listener() {
    629         @Override
    630         public void onOutsideClick() {
    631             // If there is no background, we want to dismiss, because to the user it seems
    632             // like he had touched outside. If the ViewPager is solid however, those taps
    633             // must be ignored
    634             final boolean isTransparent = mListPager.getBackground() == null;
    635             if (isTransparent) handleOutsideTouch();
    636         }
    637 
    638         @Override
    639         public void onItemClicked(final Action action, final boolean alternate) {
    640             final Runnable startAppRunnable = new Runnable() {
    641                 @Override
    642                 public void run() {
    643                     try {
    644                         startActivity(alternate ? action.getAlternateIntent() : action.getIntent());
    645                     } catch (ActivityNotFoundException e) {
    646                         Toast.makeText(QuickContactActivity.this, R.string.quickcontact_missing_app,
    647                                 Toast.LENGTH_SHORT).show();
    648                     }
    649 
    650                     hide(false);
    651                 }
    652             };
    653             // Defer the action to make the window properly repaint
    654             new Handler().post(startAppRunnable);
    655         }
    656     };
    657 
    658     private interface DataQuery {
    659         final String[] PROJECTION = new String[] {
    660                 Data._ID,
    661 
    662                 RawContacts.ACCOUNT_TYPE,
    663                 RawContacts.DATA_SET,
    664                 Contacts.STARRED,
    665                 Contacts.DISPLAY_NAME,
    666 
    667                 Data.STATUS,
    668                 Data.STATUS_RES_PACKAGE,
    669                 Data.STATUS_ICON,
    670                 Data.STATUS_LABEL,
    671                 Data.STATUS_TIMESTAMP,
    672                 Data.PRESENCE,
    673                 Data.CHAT_CAPABILITY,
    674 
    675                 Data.RES_PACKAGE,
    676                 Data.MIMETYPE,
    677                 Data.IS_PRIMARY,
    678                 Data.IS_SUPER_PRIMARY,
    679                 Data.RAW_CONTACT_ID,
    680 
    681                 Data.DATA1, Data.DATA2, Data.DATA3, Data.DATA4, Data.DATA5,
    682                 Data.DATA6, Data.DATA7, Data.DATA8, Data.DATA9, Data.DATA10, Data.DATA11,
    683                 Data.DATA12, Data.DATA13, Data.DATA14, Data.DATA15,
    684         };
    685 
    686         final int _ID = 0;
    687 
    688         final int ACCOUNT_TYPE = 1;
    689         final int DATA_SET = 2;
    690         final int STARRED = 3;
    691         final int DISPLAY_NAME = 4;
    692 
    693         final int STATUS = 5;
    694         final int STATUS_RES_PACKAGE = 6;
    695         final int STATUS_ICON = 7;
    696         final int STATUS_LABEL = 8;
    697         final int STATUS_TIMESTAMP = 9;
    698         final int PRESENCE = 10;
    699         final int CHAT_CAPABILITY = 11;
    700 
    701         final int RES_PACKAGE = 12;
    702         final int MIMETYPE = 13;
    703         final int IS_PRIMARY = 14;
    704         final int IS_SUPER_PRIMARY = 15;
    705     }
    706 }
    707