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