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                                 }
    316                             }, POST_DRAW_WAIT_DURATION);
    317                         }
    318                     });
    319                 }
    320             });
    321             if (!animated) {
    322                 // If we were in the wrong state, simply quit (this can happen for example
    323                 // if the user pushes BACK before anything has loaded)
    324                 finish();
    325             }
    326         } else {
    327             finish();
    328         }
    329     }
    330 
    331     @Override
    332     public void onBackPressed() {
    333         close(true);
    334     }
    335 
    336     /** Assign this string to the view if it is not empty. */
    337     private void setHeaderNameText(int id, int resId) {
    338         setHeaderNameText(id, getText(resId));
    339     }
    340 
    341     /** Assign this string to the view if it is not empty. */
    342     private void setHeaderNameText(int id, CharSequence value) {
    343         final View view = mPhotoContainer.findViewById(id);
    344         if (view instanceof TextView) {
    345             if (!TextUtils.isEmpty(value)) {
    346                 ((TextView)view).setText(value);
    347             }
    348         }
    349     }
    350 
    351     /**
    352      * Check if the given MIME-type appears in the list of excluded MIME-types
    353      * that the most-recent caller requested.
    354      */
    355     private boolean isMimeExcluded(String mimeType) {
    356         if (mExcludeMimes == null) return false;
    357         for (String excludedMime : mExcludeMimes) {
    358             if (TextUtils.equals(excludedMime, mimeType)) {
    359                 return true;
    360             }
    361         }
    362         return false;
    363     }
    364 
    365     /**
    366      * Handle the result from the ContactLoader
    367      */
    368     private void bindData(Contact data) {
    369         mContactData = data;
    370         final ResolveCache cache = ResolveCache.getInstance(this);
    371         final Context context = this;
    372 
    373         mOpenDetailsOrAddContactImage.setVisibility(isMimeExcluded(Contacts.CONTENT_ITEM_TYPE) ?
    374                 View.GONE : View.VISIBLE);
    375         final boolean isStarred = data.getStarred();
    376         if (isStarred) {
    377             mStarImage.setImageResource(R.drawable.ic_favorite_on_lt);
    378             mStarImage.setContentDescription(
    379                 getResources().getString(R.string.menu_removeStar));
    380         } else {
    381             mStarImage.setImageResource(R.drawable.ic_favorite_off_lt);
    382             mStarImage.setContentDescription(
    383                 getResources().getString(R.string.menu_addStar));
    384         }
    385         final Uri lookupUri = data.getLookupUri();
    386 
    387         // If this is a json encoded URI, there is no local contact to star
    388         if (UriUtils.isEncodedContactUri(lookupUri)) {
    389             mStarImage.setVisibility(View.GONE);
    390 
    391             // If directory export support is not allowed, then don't allow the user to add
    392             // to contacts
    393             if (mContactData.getDirectoryExportSupport() == Directory.EXPORT_SUPPORT_NONE) {
    394                 configureHeaderClickActions(false);
    395             } else {
    396                 configureHeaderClickActions(true);
    397             }
    398         } else {
    399             configureHeaderClickActions(false);
    400             mStarImage.setVisibility(View.VISIBLE);
    401             mStarImage.setOnClickListener(new OnClickListener() {
    402                 @Override
    403                 public void onClick(View view) {
    404                     // Toggle "starred" state
    405                     // Make sure there is a contact
    406                     if (lookupUri != null) {
    407                         // Changes the state of the image already before sending updates to the
    408                         // database
    409                         if (isStarred) {
    410                             mStarImage.setImageResource(R.drawable.ic_favorite_off_lt);
    411                         } else {
    412                             mStarImage.setImageResource(R.drawable.ic_favorite_on_lt);
    413                         }
    414 
    415                         // Now perform the real save
    416                         final Intent intent = ContactSaveService.createSetStarredIntent(context,
    417                                 lookupUri, !isStarred);
    418                         context.startService(intent);
    419                     }
    420                 }
    421             });
    422         }
    423 
    424         mDefaultsMap.clear();
    425 
    426         mStopWatch.lap("sph"); // Start photo setting
    427 
    428         mPhotoSetter.setupContactPhoto(data, mPhotoView);
    429 
    430         mStopWatch.lap("ph"); // Photo set
    431 
    432         for (RawContact rawContact : data.getRawContacts()) {
    433             for (DataItem dataItem : rawContact.getDataItems()) {
    434                 final String mimeType = dataItem.getMimeType();
    435                 final AccountType accountType = rawContact.getAccountType(this);
    436                 final DataKind dataKind = AccountTypeManager.getInstance(this)
    437                         .getKindOrFallback(accountType, mimeType);
    438 
    439                 // Skip this data item if MIME-type excluded
    440                 if (isMimeExcluded(mimeType)) continue;
    441 
    442                 final long dataId = dataItem.getId();
    443                 final boolean isPrimary = dataItem.isPrimary();
    444                 final boolean isSuperPrimary = dataItem.isSuperPrimary();
    445 
    446                 if (dataKind != null) {
    447                     // Build an action for this data entry, find a mapping to a UI
    448                     // element, build its summary from the cursor, and collect it
    449                     // along with all others of this MIME-type.
    450                     final Action action = new DataAction(context, dataItem, dataKind);
    451                     final boolean wasAdded = considerAdd(action, cache, isSuperPrimary);
    452                     if (wasAdded) {
    453                         // Remember the default
    454                         if (isSuperPrimary || (isPrimary && (mDefaultsMap.get(mimeType) == null))) {
    455                             mDefaultsMap.put(mimeType, action);
    456                         }
    457                     }
    458                 }
    459 
    460                 // Handle Email rows with presence data as Im entry
    461                 final DataStatus status = data.getStatuses().get(dataId);
    462                 if (status != null && dataItem instanceof EmailDataItem) {
    463                     final EmailDataItem email = (EmailDataItem) dataItem;
    464                     final ImDataItem im = ImDataItem.createFromEmail(email);
    465                     if (dataKind != null) {
    466                         final DataAction action = new DataAction(context, im, dataKind);
    467                         action.setPresence(status.getPresence());
    468                         considerAdd(action, cache, isSuperPrimary);
    469                     }
    470                 }
    471             }
    472         }
    473 
    474         mStopWatch.lap("e"); // Entities inflated
    475 
    476         // Collapse Action Lists (remove e.g. duplicate e-mail addresses from different sources)
    477         for (List<Action> actionChildren : mActions.values()) {
    478             Collapser.collapseList(actionChildren);
    479         }
    480 
    481         mStopWatch.lap("c"); // List collapsed
    482 
    483         setHeaderNameText(R.id.name, data.getDisplayName());
    484 
    485         // All the mime-types to add.
    486         final Set<String> containedTypes = new HashSet<String>(mActions.keySet());
    487         mSortedActionMimeTypes.clear();
    488         // First, add LEADING_MIMETYPES, which are most common.
    489         for (String mimeType : LEADING_MIMETYPES) {
    490             if (containedTypes.contains(mimeType)) {
    491                 mSortedActionMimeTypes.add(mimeType);
    492                 containedTypes.remove(mimeType);
    493             }
    494         }
    495 
    496         // Add all the remaining ones that are not TRAILING
    497         for (String mimeType : containedTypes.toArray(new String[containedTypes.size()])) {
    498             if (!TRAILING_MIMETYPES.contains(mimeType)) {
    499                 mSortedActionMimeTypes.add(mimeType);
    500                 containedTypes.remove(mimeType);
    501             }
    502         }
    503 
    504         // Then, add TRAILING_MIMETYPES, which are least common.
    505         for (String mimeType : TRAILING_MIMETYPES) {
    506             if (containedTypes.contains(mimeType)) {
    507                 containedTypes.remove(mimeType);
    508                 mSortedActionMimeTypes.add(mimeType);
    509             }
    510         }
    511         mPagerAdapter.notifyDataSetChanged();
    512 
    513         mStopWatch.lap("mt"); // Mime types initialized
    514 
    515         // Add buttons for each mimetype
    516         mTrack.removeAllViews();
    517         for (String mimeType : mSortedActionMimeTypes) {
    518             final View actionView = inflateAction(mimeType, cache, mTrack, data.getDisplayName());
    519             mTrack.addView(actionView);
    520         }
    521 
    522         mStopWatch.lap("mt"); // Buttons added
    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      * @param action the action to handle
    536      * @param resolveCache cache of applications that can handle actions
    537      * @param front indicates whether to add the action to the front of the list
    538      * @return true if action has been added
    539      */
    540     private boolean considerAdd(Action action, ResolveCache resolveCache, boolean front) {
    541         if (resolveCache.hasResolve(action)) {
    542             mActions.put(action.getMimeType(), action, front);
    543             return true;
    544         }
    545         return false;
    546     }
    547 
    548     /**
    549      * Bind the correct image resource and click handlers to the header views
    550      *
    551      * @param canAdd Whether or not the user can directly add information in this quick contact
    552      * to their local contacts
    553      */
    554     private void configureHeaderClickActions(boolean canAdd) {
    555         if (canAdd) {
    556             mOpenDetailsOrAddContactImage.setImageResource(R.drawable.ic_add_contact_holo_dark);
    557             mOpenDetailsOrAddContactImage.setOnClickListener(mAddToContactsClickHandler);
    558             mPhotoView.setOnClickListener(mAddToContactsClickHandler);
    559         } else {
    560             mOpenDetailsOrAddContactImage.setImageResource(R.drawable.ic_contacts_holo_dark);
    561             mOpenDetailsOrAddContactImage.setOnClickListener(mOpenDetailsClickHandler);
    562             mPhotoView.setOnClickListener(mOpenDetailsClickHandler);
    563         }
    564     }
    565 
    566     /**
    567      * Inflate the in-track view for the action of the given MIME-type, collapsing duplicate values.
    568      * Will use the icon provided by the {@link DataKind}.
    569      */
    570     private View inflateAction(String mimeType, ResolveCache resolveCache,
    571                                ViewGroup root, String name) {
    572         final CheckableImageView typeView = (CheckableImageView) getLayoutInflater().inflate(
    573                 R.layout.quickcontact_track_button, root, false);
    574 
    575         List<Action> children = mActions.get(mimeType);
    576         typeView.setTag(mimeType);
    577         final Action firstInfo = children.get(0);
    578 
    579         // Set icon and listen for clicks
    580         final CharSequence descrip = resolveCache.getDescription(firstInfo, name);
    581         final Drawable icon = resolveCache.getIcon(firstInfo);
    582         typeView.setChecked(false);
    583         typeView.setContentDescription(descrip);
    584         typeView.setImageDrawable(icon);
    585         typeView.setOnClickListener(mTypeViewClickListener);
    586 
    587         return typeView;
    588     }
    589 
    590     private CheckableImageView getActionViewAt(int position) {
    591         return (CheckableImageView) mTrack.getChildAt(position);
    592     }
    593 
    594     @Override
    595     public void onAttachFragment(Fragment fragment) {
    596         final QuickContactListFragment listFragment = (QuickContactListFragment) fragment;
    597         listFragment.setListener(mListFragmentListener);
    598     }
    599 
    600     private LoaderCallbacks<Contact> mLoaderCallbacks =
    601             new LoaderCallbacks<Contact>() {
    602         @Override
    603         public void onLoaderReset(Loader<Contact> loader) {
    604         }
    605 
    606         @Override
    607         public void onLoadFinished(Loader<Contact> loader, Contact data) {
    608             mStopWatch.lap("lf"); // onLoadFinished
    609             if (isFinishing()) {
    610                 close(false);
    611                 return;
    612             }
    613             if (data.isError()) {
    614                 // This shouldn't ever happen, so throw an exception. The {@link ContactLoader}
    615                 // should log the actual exception.
    616                 throw new IllegalStateException("Failed to load contact", data.getException());
    617             }
    618             if (data.isNotFound()) {
    619                 Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri());
    620                 Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage,
    621                         Toast.LENGTH_LONG).show();
    622                 close(false);
    623                 return;
    624             }
    625 
    626             bindData(data);
    627 
    628             mStopWatch.lap("bd"); // bindData finished
    629 
    630             if (TRACE_LAUNCH) android.os.Debug.stopMethodTracing();
    631             if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
    632                 Log.d(Constants.PERFORMANCE_TAG, "QuickContact shown");
    633             }
    634 
    635             // Data bound and ready, pull curtain to show. Put this on the Handler to ensure
    636             // that the layout passes are completed
    637             SchedulingUtils.doAfterLayout(mFloatingLayout, new Runnable() {
    638                 @Override
    639                 public void run() {
    640                     mFloatingLayout.showContent(new Runnable() {
    641                         @Override
    642                         public void run() {
    643                             mContactLoader.upgradeToFullContact();
    644                         }
    645                     });
    646                 }
    647             });
    648             mStopWatch.stopAndLog(TAG, 0);
    649             mStopWatch = StopWatch.getNullStopWatch(); // We're done with it.
    650         }
    651 
    652         @Override
    653         public Loader<Contact> onCreateLoader(int id, Bundle args) {
    654             if (mLookupUri == null) {
    655                 Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early");
    656             }
    657             return new ContactLoader(getApplicationContext(), mLookupUri,
    658                     false /*loadGroupMetaData*/, false /*loadInvitableAccountTypes*/,
    659                     false /*postViewNotification*/, true /*computeFormattedPhoneNumber*/);
    660         }
    661     };
    662 
    663     /** A type (e.g. Call/Addresses was clicked) */
    664     private final OnClickListener mTypeViewClickListener = new OnClickListener() {
    665         @Override
    666         public void onClick(View view) {
    667             final CheckableImageView actionView = (CheckableImageView)view;
    668             final String mimeType = (String) actionView.getTag();
    669             int index = mSortedActionMimeTypes.indexOf(mimeType);
    670             mListPager.setCurrentItem(index, true);
    671         }
    672     };
    673 
    674     private class ViewPagerAdapter extends FragmentPagerAdapter {
    675         public ViewPagerAdapter(FragmentManager fragmentManager) {
    676             super(fragmentManager);
    677         }
    678 
    679         @Override
    680         public Fragment getItem(int position) {
    681             final String mimeType = mSortedActionMimeTypes.get(position);
    682             QuickContactListFragment fragment = new QuickContactListFragment(mimeType);
    683             final List<Action> actions = mActions.get(mimeType);
    684             fragment.setActions(actions);
    685             return fragment;
    686         }
    687 
    688         @Override
    689         public int getCount() {
    690             return mSortedActionMimeTypes.size();
    691         }
    692 
    693         @Override
    694         public int getItemPosition(Object object) {
    695             final QuickContactListFragment fragment = (QuickContactListFragment) object;
    696             final String mimeType = fragment.getMimeType();
    697             for (int i = 0; i < mSortedActionMimeTypes.size(); i++) {
    698                 if (mimeType.equals(mSortedActionMimeTypes.get(i))) {
    699                     return i;
    700                 }
    701             }
    702             return PagerAdapter.POSITION_NONE;
    703         }
    704     }
    705 
    706     private class PageChangeListener extends SimpleOnPageChangeListener {
    707         private int mScrollingState = ViewPager.SCROLL_STATE_IDLE;
    708 
    709         @Override
    710         public void onPageSelected(int position) {
    711             final CheckableImageView actionView = getActionViewAt(position);
    712             mTrackScroller.requestChildRectangleOnScreen(actionView,
    713                     new Rect(0, 0, actionView.getWidth(), actionView.getHeight()), false);
    714             // Don't render rectangle if we are currently scrolling to prevent it from flickering
    715             if (mScrollingState == ViewPager.SCROLL_STATE_IDLE) {
    716                 renderSelectedRectangle(position, 0);
    717             }
    718         }
    719 
    720         @Override
    721         public void onPageScrollStateChanged(int state) {
    722             super.onPageScrollStateChanged(state);
    723             mScrollingState = state;
    724         }
    725 
    726         @Override
    727         public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
    728             renderSelectedRectangle(position, positionOffset);
    729         }
    730 
    731         private void renderSelectedRectangle(int position, float positionOffset) {
    732             final RelativeLayout.LayoutParams layoutParams =
    733                     (RelativeLayout.LayoutParams) mSelectedTabRectangle.getLayoutParams();
    734             final int width = layoutParams.width;
    735             layoutParams.setMarginStart((int) ((position + positionOffset) * width));
    736             mSelectedTabRectangle.setLayoutParams(layoutParams);
    737         }
    738     }
    739 
    740     private final QuickContactListFragment.Listener mListFragmentListener =
    741             new QuickContactListFragment.Listener() {
    742         @Override
    743         public void onOutsideClick() {
    744             // If there is no background, we want to dismiss, because to the user it seems
    745             // like he had touched outside. If the ViewPager is solid however, those taps
    746             // must be ignored
    747             final boolean isTransparent = mListPager.getBackground() == null;
    748             if (isTransparent) handleOutsideTouch();
    749         }
    750 
    751         @Override
    752         public void onItemClicked(final Action action, final boolean alternate) {
    753             final Runnable startAppRunnable = new Runnable() {
    754                 @Override
    755                 public void run() {
    756                     try {
    757                         startActivity(alternate ? action.getAlternateIntent() : action.getIntent());
    758                     } catch (ActivityNotFoundException e) {
    759                         Toast.makeText(QuickContactActivity.this, R.string.quickcontact_missing_app,
    760                                 Toast.LENGTH_SHORT).show();
    761                     }
    762 
    763                     close(false);
    764                 }
    765             };
    766             // Defer the action to make the window properly repaint
    767             new Handler().post(startAppRunnable);
    768         }
    769     };
    770 }
    771