Home | History | Annotate | Download | only in ui
      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.ui;
     18 
     19 import com.android.contacts.Collapser;
     20 import com.android.contacts.ContactPresenceIconUtil;
     21 import com.android.contacts.ContactsUtils;
     22 import com.android.contacts.R;
     23 import com.android.contacts.StickyTabs;
     24 import com.android.contacts.model.ContactsSource;
     25 import com.android.contacts.model.Sources;
     26 import com.android.contacts.model.ContactsSource.DataKind;
     27 import com.android.contacts.ui.widget.CheckableImageView;
     28 import com.android.contacts.util.Constants;
     29 import com.android.contacts.util.DataStatus;
     30 import com.android.contacts.util.NotifyingAsyncQueryHandler;
     31 import com.android.internal.policy.PolicyManager;
     32 import com.google.android.collect.Sets;
     33 
     34 import android.content.ActivityNotFoundException;
     35 import android.content.ContentUris;
     36 import android.content.ContentValues;
     37 import android.content.Context;
     38 import android.content.Intent;
     39 import android.content.IntentFilter;
     40 import android.content.pm.ApplicationInfo;
     41 import android.content.pm.PackageManager;
     42 import android.content.pm.ResolveInfo;
     43 import android.content.res.Resources;
     44 import android.database.Cursor;
     45 import android.graphics.Bitmap;
     46 import android.graphics.BitmapFactory;
     47 import android.graphics.Rect;
     48 import android.graphics.drawable.Drawable;
     49 import android.net.Uri;
     50 import android.provider.ContactsContract.Contacts;
     51 import android.provider.ContactsContract.Data;
     52 import android.provider.ContactsContract.QuickContact;
     53 import android.provider.ContactsContract.RawContacts;
     54 import android.provider.ContactsContract.StatusUpdates;
     55 import android.provider.ContactsContract.CommonDataKinds.Email;
     56 import android.provider.ContactsContract.CommonDataKinds.Im;
     57 import android.provider.ContactsContract.CommonDataKinds.Phone;
     58 import android.provider.ContactsContract.CommonDataKinds.Photo;
     59 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
     60 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
     61 import android.provider.ContactsContract.CommonDataKinds.Website;
     62 import android.text.TextUtils;
     63 import android.util.AttributeSet;
     64 import android.util.Log;
     65 import android.view.ContextThemeWrapper;
     66 import android.view.Gravity;
     67 import android.view.KeyEvent;
     68 import android.view.LayoutInflater;
     69 import android.view.Menu;
     70 import android.view.MenuItem;
     71 import android.view.MotionEvent;
     72 import android.view.View;
     73 import android.view.ViewGroup;
     74 import android.view.ViewStub;
     75 import android.view.Window;
     76 import android.view.WindowManager;
     77 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
     78 import android.view.accessibility.AccessibilityEvent;
     79 import android.view.animation.Animation;
     80 import android.view.animation.AnimationUtils;
     81 import android.view.animation.Interpolator;
     82 import android.widget.AbsListView;
     83 import android.widget.AdapterView;
     84 import android.widget.BaseAdapter;
     85 import android.widget.CheckBox;
     86 import android.widget.CompoundButton;
     87 import android.widget.HorizontalScrollView;
     88 import android.widget.ImageView;
     89 import android.widget.ListView;
     90 import android.widget.RelativeLayout;
     91 import android.widget.TextView;
     92 import android.widget.Toast;
     93 
     94 import java.lang.ref.SoftReference;
     95 import java.util.ArrayList;
     96 import java.util.Arrays;
     97 import java.util.HashMap;
     98 import java.util.HashSet;
     99 import java.util.LinkedList;
    100 import java.util.List;
    101 import java.util.Set;
    102 
    103 /**
    104  * Window that shows QuickContact dialog for a specific {@link Contacts#_ID}.
    105  */
    106 public class QuickContactWindow implements Window.Callback,
    107         NotifyingAsyncQueryHandler.AsyncQueryListener, View.OnClickListener,
    108         AbsListView.OnItemClickListener, CompoundButton.OnCheckedChangeListener, KeyEvent.Callback,
    109         OnGlobalLayoutListener {
    110     private static final String TAG = "QuickContactWindow";
    111 
    112     /**
    113      * Interface used to allow the person showing a {@link QuickContactWindow} to
    114      * know when the window has been dismissed.
    115      */
    116     public interface OnDismissListener {
    117         public void onDismiss(QuickContactWindow dialog);
    118     }
    119 
    120     /**
    121      * Custom layout the sole purpose of which is to intercept the BACK key and
    122      * close QC even when the soft keyboard is open.
    123      */
    124     public static class RootLayout extends RelativeLayout {
    125 
    126         QuickContactWindow mQuickContactWindow;
    127 
    128         public RootLayout(Context context, AttributeSet attrs) {
    129             super(context, attrs);
    130         }
    131 
    132         /**
    133          * Intercepts the BACK key event and dismisses QuickContact window.
    134          */
    135         @Override
    136         public boolean dispatchKeyEventPreIme(KeyEvent event) {
    137             if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
    138                 mQuickContactWindow.onBackPressed();
    139                 return true;
    140             } else {
    141                 return super.dispatchKeyEventPreIme(event);
    142             }
    143         }
    144     }
    145 
    146     private final Context mContext;
    147     private final LayoutInflater mInflater;
    148     private final WindowManager mWindowManager;
    149     private Window mWindow;
    150     private View mDecor;
    151     private final Rect mRect = new Rect();
    152 
    153     private boolean mDismissed = false;
    154     private boolean mQuerying = false;
    155     private boolean mShowing = false;
    156 
    157     private NotifyingAsyncQueryHandler mHandler;
    158     private OnDismissListener mDismissListener;
    159     private ResolveCache mResolveCache;
    160 
    161     /** Last selected tab of the Dialtacs-Activity. This is -1 if not called out of contacts app */
    162     private int mLastSelectedContactsAppTab;
    163 
    164     private Uri mLookupUri;
    165     private Rect mAnchor;
    166 
    167     private int mShadowHoriz;
    168     private int mShadowVert;
    169     private int mShadowTouch;
    170 
    171     private int mScreenWidth;
    172     private int mScreenHeight;
    173     private int mRequestedY;
    174 
    175     private boolean mHasValidSocial = false;
    176     private boolean mHasData = false;
    177     private boolean mMakePrimary = false;
    178 
    179     private ImageView mArrowUp;
    180     private ImageView mArrowDown;
    181 
    182     private int mMode;
    183     private RootLayout mRootView;
    184     private View mHeader;
    185     private HorizontalScrollView mTrackScroll;
    186     private ViewGroup mTrack;
    187     private Animation mTrackAnim;
    188 
    189     private View mFooter;
    190     private View mFooterDisambig;
    191     private ListView mResolveList;
    192     private CheckableImageView mLastAction;
    193     private CheckBox mSetPrimaryCheckBox;
    194 
    195     private int mWindowRecycled = 0;
    196     private int mActionRecycled = 0;
    197 
    198     /**
    199      * Set of {@link Action} that are associated with the aggregate currently
    200      * displayed by this dialog, represented as a map from {@link String}
    201      * MIME-type to {@link ActionList}.
    202      */
    203     private ActionMap mActions = new ActionMap();
    204 
    205     /**
    206      * Pool of unused {@link CheckableImageView} that have previously been
    207      * inflated, and are ready to be recycled through {@link #obtainView()}.
    208      */
    209     private LinkedList<View> mActionPool = new LinkedList<View>();
    210 
    211     private String[] mExcludeMimes;
    212 
    213     /**
    214      * {@link #PRECEDING_MIMETYPES} and {@link #FOLLOWING_MIMETYPES} are used to sort MIME-types.
    215      *
    216      * <p>The MIME-types in {@link #PRECEDING_MIMETYPES} appear in the front of the dialog,
    217      * in the order in the array.
    218      *
    219      * <p>The ones in {@link #FOLLOWING_MIMETYPES} appear in the end of the dialog, in alphabetical
    220      * order.
    221      *
    222      * <p>The rest go between them, in the order in the array.
    223      */
    224     private static final String[] PRECEDING_MIMETYPES = new String[] {
    225             Phone.CONTENT_ITEM_TYPE,
    226             SipAddress.CONTENT_ITEM_TYPE,
    227             Contacts.CONTENT_ITEM_TYPE,
    228             Constants.MIME_SMS_ADDRESS,
    229             Email.CONTENT_ITEM_TYPE,
    230     };
    231 
    232     /**
    233      * See {@link #PRECEDING_MIMETYPES}.
    234      */
    235     private static final String[] FOLLOWING_MIMETYPES = new String[] {
    236             StructuredPostal.CONTENT_ITEM_TYPE,
    237             Website.CONTENT_ITEM_TYPE,
    238     };
    239 
    240     /**
    241      * Specific list {@link ApplicationInfo#packageName} of apps that are
    242      * prefered <strong>only</strong> for the purposes of default icons when
    243      * multiple {@link ResolveInfo} are found to match. This only happens when
    244      * the user has not selected a default app yet, and they will still be
    245      * presented with the system disambiguation dialog.
    246      */
    247     private static final HashSet<String> sPreferResolve = Sets.newHashSet(
    248             "com.android.email",
    249             "com.android.calendar",
    250             "com.android.contacts",
    251             "com.android.mms",
    252             "com.android.phone",
    253             "com.android.browser");
    254 
    255     private static final int TOKEN_DATA = 1;
    256 
    257     static final boolean LOGD = false;
    258 
    259     static final boolean TRACE_LAUNCH = false;
    260     static final String TRACE_TAG = "quickcontact";
    261 
    262     /**
    263      * Prepare a dialog to show in the given {@link Context}.
    264      */
    265     public QuickContactWindow(Context context) {
    266         mContext = new ContextThemeWrapper(context, R.style.QuickContact);
    267         mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    268         mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
    269 
    270         mWindow = PolicyManager.makeNewWindow(mContext);
    271         mWindow.setCallback(this);
    272         mWindow.setWindowManager(mWindowManager, null, null);
    273         mWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED);
    274 
    275         mWindow.setContentView(R.layout.quickcontact);
    276 
    277         mRootView = (RootLayout)mWindow.findViewById(R.id.root);
    278         mRootView.mQuickContactWindow = this;
    279         mRootView.setFocusable(true);
    280         mRootView.setFocusableInTouchMode(true);
    281         mRootView.setDescendantFocusability(RootLayout.FOCUS_AFTER_DESCENDANTS);
    282 
    283         mArrowUp = (ImageView)mWindow.findViewById(R.id.arrow_up);
    284         mArrowDown = (ImageView)mWindow.findViewById(R.id.arrow_down);
    285 
    286         mResolveCache = new ResolveCache(mContext);
    287 
    288         final Resources res = mContext.getResources();
    289         mShadowHoriz = res.getDimensionPixelSize(R.dimen.quickcontact_shadow_horiz);
    290         mShadowVert = res.getDimensionPixelSize(R.dimen.quickcontact_shadow_vert);
    291         mShadowTouch = res.getDimensionPixelSize(R.dimen.quickcontact_shadow_touch);
    292 
    293         mScreenWidth = mWindowManager.getDefaultDisplay().getWidth();
    294         mScreenHeight = mWindowManager.getDefaultDisplay().getHeight();
    295 
    296         mTrack = (ViewGroup)mWindow.findViewById(R.id.quickcontact);
    297         mTrackScroll = (HorizontalScrollView)mWindow.findViewById(R.id.scroll);
    298 
    299         mFooter = mWindow.findViewById(R.id.footer);
    300         mFooterDisambig = mWindow.findViewById(R.id.footer_disambig);
    301         mResolveList = (ListView)mWindow.findViewById(android.R.id.list);
    302         mSetPrimaryCheckBox = (CheckBox)mWindow.findViewById(android.R.id.checkbox);
    303 
    304         mSetPrimaryCheckBox.setOnCheckedChangeListener(this);
    305 
    306         // Prepare track entrance animation
    307         mTrackAnim = AnimationUtils.loadAnimation(mContext, R.anim.quickcontact);
    308         mTrackAnim.setInterpolator(new Interpolator() {
    309             public float getInterpolation(float t) {
    310                 // Pushes past the target area, then snaps back into place.
    311                 // Equation for graphing: 1.2-((x*1.6)-1.1)^2
    312                 final float inner = (t * 1.55f) - 1.1f;
    313                 return 1.2f - inner * inner;
    314             }
    315         });
    316 
    317         mHandler = new NotifyingAsyncQueryHandler(mContext, this);
    318     }
    319 
    320     /**
    321      * Prepare a dialog to show in the given {@link Context}, and notify the
    322      * given {@link OnDismissListener} each time this dialog is dismissed.
    323      */
    324     public QuickContactWindow(Context context, OnDismissListener dismissListener) {
    325         this(context);
    326         mDismissListener = dismissListener;
    327     }
    328 
    329     public void setLastSelectedContactsAppTab(int value) {
    330         mLastSelectedContactsAppTab = value;
    331     }
    332 
    333     private View getHeaderView(int mode) {
    334         View header = null;
    335         switch (mode) {
    336             case QuickContact.MODE_SMALL:
    337                 header = mWindow.findViewById(R.id.header_small);
    338                 break;
    339             case QuickContact.MODE_MEDIUM:
    340                 header = mWindow.findViewById(R.id.header_medium);
    341                 break;
    342             case QuickContact.MODE_LARGE:
    343                 header = mWindow.findViewById(R.id.header_large);
    344                 break;
    345         }
    346 
    347         if (header instanceof ViewStub) {
    348             // Inflate actual header if we picked a stub
    349             final ViewStub stub = (ViewStub)header;
    350             header = stub.inflate();
    351         } else if (header != null) {
    352             header.setVisibility(View.VISIBLE);
    353         }
    354 
    355         return header;
    356     }
    357 
    358     /**
    359      * Start showing a dialog for the given {@link Contacts#_ID} pointing
    360      * towards the given location.
    361      */
    362     public synchronized void show(Uri lookupUri, Rect anchor, int mode, String[] excludeMimes) {
    363         if (mQuerying || mShowing) {
    364             Log.w(TAG, "dismissing before showing");
    365             dismissInternal();
    366         }
    367 
    368         if (TRACE_LAUNCH && !android.os.Debug.isMethodTracingActive()) {
    369             android.os.Debug.startMethodTracing(TRACE_TAG);
    370         }
    371 
    372         // Validate incoming parameters
    373         final boolean validMode = (mode == QuickContact.MODE_SMALL
    374                 || mode == QuickContact.MODE_MEDIUM || mode == QuickContact.MODE_LARGE);
    375         if (!validMode) {
    376             throw new IllegalArgumentException("Invalid mode, expecting MODE_LARGE, "
    377                     + "MODE_MEDIUM, or MODE_SMALL");
    378         }
    379 
    380         if (anchor == null) {
    381             throw new IllegalArgumentException("Missing anchor rectangle");
    382         }
    383 
    384         // Prepare header view for requested mode
    385         mLookupUri = lookupUri;
    386         mAnchor = new Rect(anchor);
    387         mMode = mode;
    388         mExcludeMimes = excludeMimes;
    389 
    390         mHeader = getHeaderView(mode);
    391 
    392         setHeaderText(R.id.name, R.string.quickcontact_missing_name);
    393 
    394         setHeaderText(R.id.status, null);
    395         setHeaderText(R.id.timestamp, null);
    396 
    397         setHeaderImage(R.id.presence, null);
    398 
    399         resetTrack();
    400 
    401         // We need to have a focused view inside the QuickContact window so
    402         // that the BACK key event can be intercepted
    403         mRootView.requestFocus();
    404 
    405         mHasValidSocial = false;
    406         mDismissed = false;
    407         mQuerying = true;
    408 
    409         // Start background query for data, but only select photo rows when they
    410         // directly match the super-primary PHOTO_ID.
    411         final Uri dataUri = getDataUri(lookupUri);
    412         mHandler.cancelOperation(TOKEN_DATA);
    413 
    414         // Only request photo data when required by mode
    415         if (mMode == QuickContact.MODE_LARGE) {
    416             // Select photos, but only super-primary
    417             mHandler.startQuery(TOKEN_DATA, lookupUri, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
    418                     + "!=? OR (" + Data.MIMETYPE + "=? AND " + Data._ID + "=" + Contacts.PHOTO_ID
    419                     + ")", new String[] { Photo.CONTENT_ITEM_TYPE, Photo.CONTENT_ITEM_TYPE }, null);
    420         } else {
    421             // Exclude all photos from cursor
    422             mHandler.startQuery(TOKEN_DATA, lookupUri, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
    423                     + "!=?", new String[] { Photo.CONTENT_ITEM_TYPE }, null);
    424         }
    425     }
    426 
    427     /**
    428      * Build a {@link Uri} into the {@link Data} table for the requested
    429      * {@link Contacts#CONTENT_LOOKUP_URI} style {@link Uri}.
    430      */
    431     private Uri getDataUri(Uri lookupUri) {
    432         // TODO: Formalize method of extracting LOOKUP_KEY
    433         final List<String> path = lookupUri.getPathSegments();
    434         final boolean validLookup = path.size() >= 3 && "lookup".equals(path.get(1));
    435         if (!validLookup) {
    436             // We only accept valid lookup-style Uris
    437             throw new IllegalArgumentException("Expecting lookup-style Uri");
    438         } else if (path.size() == 3) {
    439             // No direct _ID provided, so force a lookup
    440             lookupUri = Contacts.lookupContact(mContext.getContentResolver(), lookupUri);
    441         }
    442 
    443         final long contactId = ContentUris.parseId(lookupUri);
    444         return Uri.withAppendedPath(ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId),
    445                 Contacts.Data.CONTENT_DIRECTORY);
    446     }
    447 
    448     /**
    449      * Show the correct call-out arrow based on a {@link R.id} reference.
    450      */
    451     private void showArrow(int whichArrow, int requestedX) {
    452         final View showArrow = (whichArrow == R.id.arrow_up) ? mArrowUp : mArrowDown;
    453         final View hideArrow = (whichArrow == R.id.arrow_up) ? mArrowDown : mArrowUp;
    454 
    455         final int arrowWidth = mArrowUp.getMeasuredWidth();
    456 
    457         showArrow.setVisibility(View.VISIBLE);
    458         ViewGroup.MarginLayoutParams param = (ViewGroup.MarginLayoutParams)showArrow.getLayoutParams();
    459         param.leftMargin = requestedX - arrowWidth / 2;
    460 
    461         hideArrow.setVisibility(View.INVISIBLE);
    462     }
    463 
    464     /**
    465      * Actual internal method to show this dialog. Called only by
    466      * {@link #considerShowing()} when all data requirements have been met.
    467      */
    468     private void showInternal() {
    469         mDecor = mWindow.getDecorView();
    470         mDecor.getViewTreeObserver().addOnGlobalLayoutListener(this);
    471         WindowManager.LayoutParams l = mWindow.getAttributes();
    472 
    473         l.width = mScreenWidth + mShadowHoriz + mShadowHoriz;
    474         l.height = WindowManager.LayoutParams.WRAP_CONTENT;
    475 
    476         // Force layout measuring pass so we have baseline numbers
    477         mDecor.measure(l.width, l.height);
    478         final int blockHeight = mDecor.getMeasuredHeight();
    479 
    480         l.gravity = Gravity.TOP | Gravity.LEFT;
    481         l.x = -mShadowHoriz;
    482 
    483         if (mAnchor.top > blockHeight) {
    484             // Show downwards callout when enough room, aligning bottom block
    485             // edge with top of anchor area, and adjusting to inset arrow.
    486             showArrow(R.id.arrow_down, mAnchor.centerX());
    487             l.y = mAnchor.top - blockHeight + mShadowVert;
    488             l.windowAnimations = R.style.QuickContactAboveAnimation;
    489 
    490         } else {
    491             // Otherwise show upwards callout, aligning block top with bottom of
    492             // anchor area, and adjusting to inset arrow.
    493             showArrow(R.id.arrow_up, mAnchor.centerX());
    494             l.y = mAnchor.bottom - mShadowVert;
    495             l.windowAnimations = R.style.QuickContactBelowAnimation;
    496 
    497         }
    498 
    499         l.flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
    500                 | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
    501 
    502         mRequestedY = l.y;
    503         mWindowManager.addView(mDecor, l);
    504         mShowing = true;
    505         mQuerying = false;
    506         mDismissed = false;
    507 
    508         mTrack.startAnimation(mTrackAnim);
    509 
    510         if (TRACE_LAUNCH) {
    511             android.os.Debug.stopMethodTracing();
    512             Log.d(TAG, "Window recycled " + mWindowRecycled + " times, chiclets "
    513                     + mActionRecycled + " times");
    514         }
    515     }
    516 
    517     /** {@inheritDoc} */
    518     public void onGlobalLayout() {
    519         layoutInScreen();
    520     }
    521 
    522     /**
    523      * Adjust vertical {@link WindowManager.LayoutParams} to fit window as best
    524      * as possible, shifting up to display content as needed.
    525      */
    526     private void layoutInScreen() {
    527         if (!mShowing) return;
    528 
    529         final WindowManager.LayoutParams l = mWindow.getAttributes();
    530         final int originalY = l.y;
    531 
    532         final int blockHeight = mDecor.getHeight();
    533 
    534         l.y = mRequestedY;
    535         if (mRequestedY + blockHeight > mScreenHeight) {
    536             // Shift up from bottom when overflowing
    537             l.y = mScreenHeight - blockHeight;
    538         }
    539 
    540         if (originalY != l.y) {
    541             // Only update when value is changed
    542             mWindow.setAttributes(l);
    543         }
    544     }
    545 
    546     /**
    547      * Dismiss this dialog if showing.
    548      */
    549     public synchronized void dismiss() {
    550         // Notify any listeners that we've been dismissed
    551         if (mDismissListener != null) {
    552             mDismissListener.onDismiss(this);
    553         }
    554 
    555         dismissInternal();
    556     }
    557 
    558     private void dismissInternal() {
    559         // Remove any attached window decor for recycling
    560         boolean hadDecor = mDecor != null;
    561         if (hadDecor) {
    562             mWindowManager.removeView(mDecor);
    563             mWindowRecycled++;
    564             mDecor.getViewTreeObserver().removeGlobalOnLayoutListener(this);
    565             mDecor = null;
    566             mWindow.closeAllPanels();
    567         }
    568         mShowing = false;
    569         mDismissed = true;
    570 
    571         // Cancel any pending queries
    572         mHandler.cancelOperation(TOKEN_DATA);
    573         mQuerying = false;
    574 
    575         // Completely hide header and reset track
    576         mHeader.setVisibility(View.GONE);
    577         resetTrack();
    578     }
    579 
    580     /**
    581      * Reset track to initial state, recycling any chiclets.
    582      */
    583     private void resetTrack() {
    584         // Release reference to last chiclet
    585         mLastAction = null;
    586 
    587         // Clear track actions and scroll to hard left
    588         mResolveCache.clear();
    589         mActions.clear();
    590 
    591         // Recycle any chiclets in use
    592         while (mTrack.getChildCount() > 2) {
    593             this.releaseView(mTrack.getChildAt(1));
    594             mTrack.removeViewAt(1);
    595         }
    596 
    597         mTrackScroll.fullScroll(View.FOCUS_LEFT);
    598         mWasDownArrow = false;
    599 
    600         // Clear any primary requests
    601         mMakePrimary = false;
    602         mSetPrimaryCheckBox.setChecked(false);
    603 
    604         setResolveVisible(false, null);
    605     }
    606 
    607     /**
    608      * Consider showing this window, which will only call through to
    609      * {@link #showInternal()} when all data items are present.
    610      */
    611     private void considerShowing() {
    612         if (mHasData && !mShowing && !mDismissed) {
    613             if (mMode == QuickContact.MODE_MEDIUM && !mHasValidSocial) {
    614                 // Missing valid social, swap medium for small header
    615                 mHeader.setVisibility(View.GONE);
    616                 mHeader = getHeaderView(QuickContact.MODE_SMALL);
    617             }
    618 
    619             // All queries have returned, pull curtain
    620             showInternal();
    621         }
    622     }
    623 
    624     /** {@inheritDoc} */
    625     public synchronized void onQueryComplete(int token, Object cookie, Cursor cursor) {
    626         // Bail early when query is stale
    627         if (cookie != mLookupUri) return;
    628 
    629         if (cursor == null) {
    630             // Problem while running query, so bail without showing
    631             Log.w(TAG, "Missing cursor for token=" + token);
    632             this.dismiss();
    633             return;
    634         }
    635 
    636         handleData(cursor);
    637         mHasData = true;
    638 
    639         if (!cursor.isClosed()) {
    640             cursor.close();
    641         }
    642 
    643         considerShowing();
    644     }
    645 
    646     /** Assign this string to the view, if found in {@link #mHeader}. */
    647     private void setHeaderText(int id, int resId) {
    648         setHeaderText(id, mContext.getResources().getText(resId));
    649     }
    650 
    651     /** Assign this string to the view, if found in {@link #mHeader}. */
    652     private void setHeaderText(int id, CharSequence value) {
    653         final View view = mHeader.findViewById(id);
    654         if (view instanceof TextView) {
    655             ((TextView)view).setText(value);
    656             view.setVisibility(TextUtils.isEmpty(value) ? View.GONE : View.VISIBLE);
    657         }
    658     }
    659 
    660     /** Assign this image to the view, if found in {@link #mHeader}. */
    661     private void setHeaderImage(int id, Drawable drawable) {
    662         final View view = mHeader.findViewById(id);
    663         if (view instanceof ImageView) {
    664             ((ImageView)view).setImageDrawable(drawable);
    665             view.setVisibility(drawable == null ? View.GONE : View.VISIBLE);
    666         }
    667     }
    668 
    669     /**
    670      * Find the QuickContact-specific presence icon for showing in chiclets.
    671      */
    672     private Drawable getTrackPresenceIcon(int status) {
    673         int resId;
    674         switch (status) {
    675             case StatusUpdates.AVAILABLE:
    676                 resId = R.drawable.quickcontact_slider_presence_active;
    677                 break;
    678             case StatusUpdates.IDLE:
    679             case StatusUpdates.AWAY:
    680                 resId = R.drawable.quickcontact_slider_presence_away;
    681                 break;
    682             case StatusUpdates.DO_NOT_DISTURB:
    683                 resId = R.drawable.quickcontact_slider_presence_busy;
    684                 break;
    685             case StatusUpdates.INVISIBLE:
    686                 resId = R.drawable.quickcontact_slider_presence_inactive;
    687                 break;
    688             case StatusUpdates.OFFLINE:
    689             default:
    690                 resId = R.drawable.quickcontact_slider_presence_inactive;
    691         }
    692         return mContext.getResources().getDrawable(resId);
    693     }
    694 
    695     /** Read {@link String} from the given {@link Cursor}. */
    696     private static String getAsString(Cursor cursor, String columnName) {
    697         final int index = cursor.getColumnIndex(columnName);
    698         return cursor.getString(index);
    699     }
    700 
    701     /** Read {@link Integer} from the given {@link Cursor}. */
    702     private static int getAsInt(Cursor cursor, String columnName) {
    703         final int index = cursor.getColumnIndex(columnName);
    704         return cursor.getInt(index);
    705     }
    706 
    707     /**
    708      * Abstract definition of an action that could be performed, along with
    709      * string description and icon.
    710      */
    711     private interface Action extends Collapser.Collapsible<Action> {
    712         public CharSequence getHeader();
    713         public CharSequence getBody();
    714 
    715         public String getMimeType();
    716         public Drawable getFallbackIcon();
    717 
    718         /**
    719          * Build an {@link Intent} that will perform this action.
    720          */
    721         public Intent getIntent();
    722 
    723         /**
    724          * Checks if the contact data for this action is primary.
    725          */
    726         public Boolean isPrimary();
    727 
    728         /**
    729          * Returns a lookup (@link Uri) for the contact data item.
    730          */
    731         public Uri getDataUri();
    732     }
    733 
    734     /**
    735      * Description of a specific {@link Data#_ID} item, with style information
    736      * defined by a {@link DataKind}.
    737      */
    738     private static class DataAction implements Action {
    739         private final Context mContext;
    740         private final DataKind mKind;
    741         private final String mMimeType;
    742 
    743         private CharSequence mHeader;
    744         private CharSequence mBody;
    745         private Intent mIntent;
    746 
    747         private boolean mAlternate;
    748         private Uri mDataUri;
    749         private boolean mIsPrimary;
    750 
    751         /**
    752          * Create an action from common {@link Data} elements.
    753          */
    754         public DataAction(Context context, String mimeType, DataKind kind,
    755                 long dataId, Cursor cursor) {
    756             mContext = context;
    757             mKind = kind;
    758             mMimeType = mimeType;
    759 
    760             // Inflate strings from cursor
    761             mAlternate = Constants.MIME_SMS_ADDRESS.equals(mimeType);
    762             if (mAlternate && mKind.actionAltHeader != null) {
    763                 mHeader = mKind.actionAltHeader.inflateUsing(context, cursor);
    764             } else if (mKind.actionHeader != null) {
    765                 mHeader = mKind.actionHeader.inflateUsing(context, cursor);
    766             }
    767 
    768             if (getAsInt(cursor, Data.IS_SUPER_PRIMARY) != 0) {
    769                 mIsPrimary = true;
    770             }
    771 
    772             if (mKind.actionBody != null) {
    773                 mBody = mKind.actionBody.inflateUsing(context, cursor);
    774             }
    775 
    776             mDataUri = ContentUris.withAppendedId(Data.CONTENT_URI, dataId);
    777 
    778             // Handle well-known MIME-types with special care
    779             if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
    780                 final String number = getAsString(cursor, Phone.NUMBER);
    781                 if (!TextUtils.isEmpty(number)) {
    782                     final Uri callUri = Uri.fromParts(Constants.SCHEME_TEL, number, null);
    783                     mIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, callUri);
    784                 }
    785             } else if (SipAddress.CONTENT_ITEM_TYPE.equals(mimeType)) {
    786                 final String address = getAsString(cursor, SipAddress.SIP_ADDRESS);
    787                 if (!TextUtils.isEmpty(address)) {
    788                     final Uri callUri = Uri.fromParts(Constants.SCHEME_SIP, address, null);
    789                     mIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, callUri);
    790                     // Note that this item will get a SIP-specific variant
    791                     // of the "call phone" icon, rather than the standard
    792                     // app icon for the Phone app (which we show for
    793                     // regular phone numbers.)  That's because the phone
    794                     // app explicitly specifies an android:icon attribute
    795                     // for the SIP-related intent-filters in its manifest.
    796                 }
    797             } else if (Constants.MIME_SMS_ADDRESS.equals(mimeType)) {
    798                 final String number = getAsString(cursor, Phone.NUMBER);
    799                 if (!TextUtils.isEmpty(number)) {
    800                     final Uri smsUri = Uri.fromParts(Constants.SCHEME_SMSTO, number, null);
    801                     mIntent = new Intent(Intent.ACTION_SENDTO, smsUri);
    802                 }
    803 
    804             } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
    805                 final String address = getAsString(cursor, Email.DATA);
    806                 if (!TextUtils.isEmpty(address)) {
    807                     final Uri mailUri = Uri.fromParts(Constants.SCHEME_MAILTO, address, null);
    808                     mIntent = new Intent(Intent.ACTION_SENDTO, mailUri);
    809                 }
    810 
    811             } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType)) {
    812                 final String url = getAsString(cursor, Website.URL);
    813                 if (!TextUtils.isEmpty(url)) {
    814                     mIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
    815                 }
    816 
    817             } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType)) {
    818                 final boolean isEmail = Email.CONTENT_ITEM_TYPE.equals(
    819                         getAsString(cursor, Data.MIMETYPE));
    820                 if (isEmail || isProtocolValid(cursor)) {
    821                     final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK :
    822                             getAsInt(cursor, Im.PROTOCOL);
    823 
    824                     if (isEmail) {
    825                         // Use Google Talk string when using Email, and clear data
    826                         // Uri so we don't try saving Email as primary.
    827                         mHeader = context.getText(R.string.chat_gtalk);
    828                         mDataUri = null;
    829                     }
    830 
    831                     String host = getAsString(cursor, Im.CUSTOM_PROTOCOL);
    832                     String data = getAsString(cursor, isEmail ? Email.DATA : Im.DATA);
    833                     if (protocol != Im.PROTOCOL_CUSTOM) {
    834                         // Try bringing in a well-known host for specific protocols
    835                         host = ContactsUtils.lookupProviderNameFromId(protocol);
    836                     }
    837 
    838                     if (!TextUtils.isEmpty(host) && !TextUtils.isEmpty(data)) {
    839                         final String authority = host.toLowerCase();
    840                         final Uri imUri = new Uri.Builder().scheme(Constants.SCHEME_IMTO).authority(
    841                                 authority).appendPath(data).build();
    842                         mIntent = new Intent(Intent.ACTION_SENDTO, imUri);
    843                     }
    844                 }
    845             }
    846 
    847             if (mIntent == null) {
    848                 // Otherwise fall back to default VIEW action
    849                 mIntent = new Intent(Intent.ACTION_VIEW, mDataUri);
    850             }
    851 
    852             // Always launch as new task, since we're like a launcher
    853             mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
    854         }
    855 
    856         private boolean isProtocolValid(Cursor cursor) {
    857             final int columnIndex = cursor.getColumnIndex(Im.PROTOCOL);
    858             if (cursor.isNull(columnIndex)) {
    859                 return false;
    860             }
    861             try {
    862                 Integer.valueOf(cursor.getString(columnIndex));
    863             } catch (NumberFormatException e) {
    864                 return false;
    865             }
    866             return true;
    867         }
    868 
    869         /** {@inheritDoc} */
    870         public CharSequence getHeader() {
    871             return mHeader;
    872         }
    873 
    874         /** {@inheritDoc} */
    875         public CharSequence getBody() {
    876             return mBody;
    877         }
    878 
    879         /** {@inheritDoc} */
    880         public String getMimeType() {
    881             return mMimeType;
    882         }
    883 
    884         /** {@inheritDoc} */
    885         public Uri getDataUri() {
    886             return mDataUri;
    887         }
    888 
    889         /** {@inheritDoc} */
    890         public Boolean isPrimary() {
    891             return mIsPrimary;
    892         }
    893 
    894         /** {@inheritDoc} */
    895         public Drawable getFallbackIcon() {
    896             // Bail early if no valid resources
    897             final String resPackageName = mKind.resPackageName;
    898             if (resPackageName == null) return null;
    899 
    900             final PackageManager pm = mContext.getPackageManager();
    901             if (mAlternate && mKind.iconAltRes != -1) {
    902                 return pm.getDrawable(resPackageName, mKind.iconAltRes, null);
    903             } else if (mKind.iconRes != -1) {
    904                 return pm.getDrawable(resPackageName, mKind.iconRes, null);
    905             } else {
    906                 return null;
    907             }
    908         }
    909 
    910         /** {@inheritDoc} */
    911         public Intent getIntent() {
    912             return mIntent;
    913         }
    914 
    915         /** {@inheritDoc} */
    916         public boolean collapseWith(Action other) {
    917             if (!shouldCollapseWith(other)) {
    918                 return false;
    919             }
    920             return true;
    921         }
    922 
    923         /** {@inheritDoc} */
    924         public boolean shouldCollapseWith(Action t) {
    925             if (t == null) {
    926                 return false;
    927             }
    928             if (!(t instanceof DataAction)) {
    929                 Log.e(TAG, "t must be DataAction");
    930                 return false;
    931             }
    932             DataAction other = (DataAction)t;
    933             if (!ContactsUtils.areObjectsEqual(mKind, other.mKind)) {
    934                 return false;
    935             }
    936             if (!ContactsUtils.shouldCollapse(mContext, mMimeType, mBody, other.mMimeType,
    937                     other.mBody)) {
    938                 return false;
    939             }
    940             if (!TextUtils.equals(mMimeType, other.mMimeType)
    941                     || !ContactsUtils.areIntentActionEqual(mIntent, other.mIntent)
    942                     ) {
    943                 return false;
    944             }
    945             return true;
    946         }
    947     }
    948 
    949     /**
    950      * Specific action that launches the profile card.
    951      */
    952     private static class ProfileAction implements Action {
    953         private final Context mContext;
    954         private final Uri mLookupUri;
    955 
    956         public ProfileAction(Context context, Uri lookupUri) {
    957             mContext = context;
    958             mLookupUri = lookupUri;
    959         }
    960 
    961         /** {@inheritDoc} */
    962         public CharSequence getHeader() {
    963             return null;
    964         }
    965 
    966         /** {@inheritDoc} */
    967         public CharSequence getBody() {
    968             return null;
    969         }
    970 
    971         /** {@inheritDoc} */
    972         public String getMimeType() {
    973             return Contacts.CONTENT_ITEM_TYPE;
    974         }
    975 
    976         /** {@inheritDoc} */
    977         public Drawable getFallbackIcon() {
    978             return mContext.getResources().getDrawable(R.drawable.ic_contacts_details);
    979         }
    980 
    981         /** {@inheritDoc} */
    982         public Intent getIntent() {
    983             final Intent intent = new Intent(Intent.ACTION_VIEW, mLookupUri);
    984 	    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
    985 	    return intent;
    986         }
    987 
    988         /** {@inheritDoc} */
    989         public Boolean isPrimary() {
    990             return null;
    991         }
    992 
    993         /** {@inheritDoc} */
    994         public Uri getDataUri() {
    995             return null;
    996         }
    997 
    998         /** {@inheritDoc} */
    999         public boolean collapseWith(Action t) {
   1000             return false; // Never dup.
   1001         }
   1002 
   1003         /** {@inheritDoc} */
   1004         public boolean shouldCollapseWith(Action t) {
   1005             return false; // Never dup.
   1006         }
   1007     }
   1008 
   1009     /**
   1010      * Internally hold a cache of scaled icons based on {@link PackageManager}
   1011      * queries, keyed internally on MIME-type.
   1012      */
   1013     private static class ResolveCache {
   1014         private PackageManager mPackageManager;
   1015 
   1016         /**
   1017          * Cached entry holding the best {@link ResolveInfo} for a specific
   1018          * MIME-type, along with a {@link SoftReference} to its icon.
   1019          */
   1020         private static class Entry {
   1021             public ResolveInfo bestResolve;
   1022             public SoftReference<Drawable> icon;
   1023         }
   1024 
   1025         private HashMap<String, Entry> mCache = new HashMap<String, Entry>();
   1026 
   1027         public ResolveCache(Context context) {
   1028             mPackageManager = context.getPackageManager();
   1029         }
   1030 
   1031         /**
   1032          * Get the {@link Entry} best associated with the given {@link Action},
   1033          * or create and populate a new one if it doesn't exist.
   1034          */
   1035         protected Entry getEntry(Action action) {
   1036             final String mimeType = action.getMimeType();
   1037             Entry entry = mCache.get(mimeType);
   1038             if (entry != null) return entry;
   1039             entry = new Entry();
   1040 
   1041             final Intent intent = action.getIntent();
   1042             if (intent != null) {
   1043                 final List<ResolveInfo> matches = mPackageManager.queryIntentActivities(intent,
   1044                         PackageManager.MATCH_DEFAULT_ONLY);
   1045 
   1046                 // Pick first match, otherwise best found
   1047                 ResolveInfo bestResolve = null;
   1048                 final int size = matches.size();
   1049                 if (size == 1) {
   1050                     bestResolve = matches.get(0);
   1051                 } else if (size > 1) {
   1052                     bestResolve = getBestResolve(intent, matches);
   1053                 }
   1054 
   1055                 if (bestResolve != null) {
   1056                     final Drawable icon = bestResolve.loadIcon(mPackageManager);
   1057 
   1058                     entry.bestResolve = bestResolve;
   1059                     entry.icon = new SoftReference<Drawable>(icon);
   1060                 }
   1061             }
   1062 
   1063             mCache.put(mimeType, entry);
   1064             return entry;
   1065         }
   1066 
   1067         /**
   1068          * Best {@link ResolveInfo} when multiple found. Ties are broken by
   1069          * selecting first from the {QuickContactWindow#sPreferResolve} list of
   1070          * preferred packages, second by apps that live on the system partition,
   1071          * otherwise the app from the top of the list. This is
   1072          * <strong>only</strong> used for selecting a default icon for
   1073          * displaying in the track, and does not shortcut the system
   1074          * {@link Intent} disambiguation dialog.
   1075          */
   1076         protected ResolveInfo getBestResolve(Intent intent, List<ResolveInfo> matches) {
   1077             // Try finding preferred activity, otherwise detect disambig
   1078             final ResolveInfo foundResolve = mPackageManager.resolveActivity(intent,
   1079                     PackageManager.MATCH_DEFAULT_ONLY);
   1080             final boolean foundDisambig = (foundResolve.match &
   1081                     IntentFilter.MATCH_CATEGORY_MASK) == 0;
   1082 
   1083             if (!foundDisambig) {
   1084                 // Found concrete match, so return directly
   1085                 return foundResolve;
   1086             }
   1087 
   1088             // Accept any package from prefer list, otherwise first system app
   1089             ResolveInfo firstSystem = null;
   1090             for (ResolveInfo info : matches) {
   1091                 final boolean isSystem = (info.activityInfo.applicationInfo.flags
   1092                         & ApplicationInfo.FLAG_SYSTEM) != 0;
   1093                 final boolean isPrefer = QuickContactWindow.sPreferResolve
   1094                         .contains(info.activityInfo.applicationInfo.packageName);
   1095 
   1096 
   1097 
   1098                 if (isPrefer) return info;
   1099                 if (isSystem && firstSystem != null) firstSystem = info;
   1100             }
   1101 
   1102             // Return first system found, otherwise first from list
   1103             return firstSystem != null ? firstSystem : matches.get(0);
   1104         }
   1105 
   1106         /**
   1107          * Check {@link PackageManager} to see if any apps offer to handle the
   1108          * given {@link Action}.
   1109          */
   1110         public boolean hasResolve(Action action) {
   1111             return getEntry(action).bestResolve != null;
   1112         }
   1113 
   1114         /**
   1115          * Find the best description for the given {@link Action}, usually used
   1116          * for accessibility purposes.
   1117          */
   1118         public CharSequence getDescription(Action action) {
   1119             final CharSequence actionHeader = action.getHeader();
   1120             final ResolveInfo info = getEntry(action).bestResolve;
   1121             if (!TextUtils.isEmpty(actionHeader)) {
   1122                 return actionHeader;
   1123             } else if (info != null) {
   1124                 return info.loadLabel(mPackageManager);
   1125             } else {
   1126                 return null;
   1127             }
   1128         }
   1129 
   1130         /**
   1131          * Return the best icon for the given {@link Action}, which is usually
   1132          * based on the {@link ResolveInfo} found through a
   1133          * {@link PackageManager} query.
   1134          */
   1135         public Drawable getIcon(Action action) {
   1136             final SoftReference<Drawable> iconRef = getEntry(action).icon;
   1137             return (iconRef == null) ? null : iconRef.get();
   1138         }
   1139 
   1140         public void clear() {
   1141             mCache.clear();
   1142         }
   1143     }
   1144 
   1145     /**
   1146      * Provide a strongly-typed {@link LinkedList} that holds a list of
   1147      * {@link Action} objects.
   1148      */
   1149     private class ActionList extends ArrayList<Action> {
   1150     }
   1151 
   1152     /**
   1153      * Provide a simple way of collecting one or more {@link Action} objects
   1154      * under a MIME-type key.
   1155      */
   1156     private class ActionMap extends HashMap<String, ActionList> {
   1157         private void collect(String mimeType, Action info) {
   1158             // Create list for this MIME-type when needed
   1159             ActionList collectList = get(mimeType);
   1160             if (collectList == null) {
   1161                 collectList = new ActionList();
   1162                 put(mimeType, collectList);
   1163             }
   1164             collectList.add(info);
   1165         }
   1166     }
   1167 
   1168     /**
   1169      * Check if the given MIME-type appears in the list of excluded MIME-types
   1170      * that the most-recent caller requested.
   1171      */
   1172     private boolean isMimeExcluded(String mimeType) {
   1173         if (mExcludeMimes == null) return false;
   1174         for (String excludedMime : mExcludeMimes) {
   1175             if (TextUtils.equals(excludedMime, mimeType)) {
   1176                 return true;
   1177             }
   1178         }
   1179         return false;
   1180     }
   1181 
   1182     /**
   1183      * Handle the result from the {@link #TOKEN_DATA} query.
   1184      */
   1185     private void handleData(Cursor cursor) {
   1186         if (cursor == null) return;
   1187 
   1188         if (!isMimeExcluded(Contacts.CONTENT_ITEM_TYPE)) {
   1189             // Add the profile shortcut action
   1190             final Action action = new ProfileAction(mContext, mLookupUri);
   1191             mActions.collect(Contacts.CONTENT_ITEM_TYPE, action);
   1192         }
   1193 
   1194         final DataStatus status = new DataStatus();
   1195         final Sources sources = Sources.getInstance(mContext);
   1196         final ImageView photoView = (ImageView)mHeader.findViewById(R.id.photo);
   1197 
   1198         Bitmap photoBitmap = null;
   1199         while (cursor.moveToNext()) {
   1200             final long dataId = cursor.getLong(DataQuery._ID);
   1201             final String accountType = cursor.getString(DataQuery.ACCOUNT_TYPE);
   1202             final String mimeType = cursor.getString(DataQuery.MIMETYPE);
   1203 
   1204             // Handle any social status updates from this row
   1205             status.possibleUpdate(cursor);
   1206 
   1207             // Skip this data item if MIME-type excluded
   1208             if (isMimeExcluded(mimeType)) continue;
   1209 
   1210             // Handle photos included as data row
   1211             if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
   1212                 final int colPhoto = cursor.getColumnIndex(Photo.PHOTO);
   1213                 final byte[] photoBlob = cursor.getBlob(colPhoto);
   1214                 if (photoBlob != null) {
   1215                     photoBitmap = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
   1216                 }
   1217                 continue;
   1218             }
   1219 
   1220             final DataKind kind = sources.getKindOrFallback(accountType, mimeType, mContext,
   1221                     ContactsSource.LEVEL_MIMETYPES);
   1222 
   1223             if (kind != null) {
   1224                 // Build an action for this data entry, find a mapping to a UI
   1225                 // element, build its summary from the cursor, and collect it
   1226                 // along with all others of this MIME-type.
   1227                 final Action action = new DataAction(mContext, mimeType, kind, dataId, cursor);
   1228                 considerAdd(action, mimeType);
   1229             }
   1230 
   1231             // If phone number, also insert as text message action
   1232             if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && kind != null) {
   1233                 final Action action = new DataAction(mContext, Constants.MIME_SMS_ADDRESS,
   1234                         kind, dataId, cursor);
   1235                 considerAdd(action, Constants.MIME_SMS_ADDRESS);
   1236             }
   1237 
   1238             // Handle Email rows with presence data as Im entry
   1239             final boolean hasPresence = !cursor.isNull(DataQuery.PRESENCE);
   1240             if (hasPresence && Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
   1241                 final DataKind imKind = sources.getKindOrFallback(accountType,
   1242                         Im.CONTENT_ITEM_TYPE, mContext, ContactsSource.LEVEL_MIMETYPES);
   1243                 if (imKind != null) {
   1244                     final Action action = new DataAction(mContext, Im.CONTENT_ITEM_TYPE, imKind,
   1245                             dataId, cursor);
   1246                     considerAdd(action, Im.CONTENT_ITEM_TYPE);
   1247                 }
   1248             }
   1249         }
   1250 
   1251         if (cursor.moveToLast()) {
   1252             // Read contact information from last data row
   1253             final String name = cursor.getString(DataQuery.DISPLAY_NAME);
   1254             final int presence = cursor.getInt(DataQuery.CONTACT_PRESENCE);
   1255             final Drawable statusIcon = ContactPresenceIconUtil.getPresenceIcon(mContext, presence);
   1256 
   1257             setHeaderText(R.id.name, name);
   1258             setHeaderImage(R.id.presence, statusIcon);
   1259         }
   1260 
   1261         if (photoView != null) {
   1262             // Place photo when discovered in data, otherwise hide
   1263             photoView.setVisibility(photoBitmap != null ? View.VISIBLE : View.GONE);
   1264             photoView.setImageBitmap(photoBitmap);
   1265         }
   1266 
   1267         mHasValidSocial = status.isValid();
   1268         if (mHasValidSocial && mMode != QuickContact.MODE_SMALL) {
   1269             // Update status when valid was found
   1270             setHeaderText(R.id.status, status.getStatus());
   1271             setHeaderText(R.id.timestamp, status.getTimestampLabel(mContext));
   1272         }
   1273 
   1274         // Turn our list of actions into UI elements
   1275 
   1276         // Index where we start adding child views.
   1277         int index = mTrack.getChildCount() - 1;
   1278 
   1279         // All the mime-types to add.
   1280         final Set<String> containedTypes = new HashSet<String>(mActions.keySet());
   1281 
   1282         // First, add PRECEDING_MIMETYPES, which are most common.
   1283         for (String mimeType : PRECEDING_MIMETYPES) {
   1284             if (containedTypes.contains(mimeType)) {
   1285                 mTrack.addView(inflateAction(mimeType), index++);
   1286                 containedTypes.remove(mimeType);
   1287             }
   1288         }
   1289 
   1290         // Keep the current index to append non PRECEDING/FOLLOWING items.
   1291         final int indexAfterPreceding = index;
   1292 
   1293         // Then, add FOLLOWING_MIMETYPES, which are least common.
   1294         for (String mimeType : FOLLOWING_MIMETYPES) {
   1295             if (containedTypes.contains(mimeType)) {
   1296                 mTrack.addView(inflateAction(mimeType), index++);
   1297                 containedTypes.remove(mimeType);
   1298             }
   1299         }
   1300 
   1301         // Go back to just after PRECEDING_MIMETYPES, and append the rest.
   1302         index = indexAfterPreceding;
   1303         final String[] remainingTypes = containedTypes.toArray(new String[containedTypes.size()]);
   1304         Arrays.sort(remainingTypes);
   1305         for (String mimeType : remainingTypes) {
   1306             mTrack.addView(inflateAction(mimeType), index++);
   1307         }
   1308     }
   1309 
   1310     /**
   1311      * Consider adding the given {@link Action}, which will only happen if
   1312      * {@link PackageManager} finds an application to handle
   1313      * {@link Action#getIntent()}.
   1314      */
   1315     private void considerAdd(Action action, String mimeType) {
   1316         if (mResolveCache.hasResolve(action)) {
   1317             mActions.collect(mimeType, action);
   1318         }
   1319     }
   1320 
   1321     /**
   1322      * Obtain a new {@link CheckableImageView} for a new chiclet, either by
   1323      * recycling one from {@link #mActionPool}, or by inflating a new one. When
   1324      * finished, use {@link #releaseView(View)} to return back into the pool for
   1325      * later recycling.
   1326      */
   1327     private synchronized View obtainView() {
   1328         View view = mActionPool.poll();
   1329         if (view == null || QuickContactActivity.FORCE_CREATE) {
   1330             view = mInflater.inflate(R.layout.quickcontact_item, mTrack, false);
   1331         }
   1332         return view;
   1333     }
   1334 
   1335     /**
   1336      * Return the given {@link CheckableImageView} into our internal pool for
   1337      * possible recycling during another pass.
   1338      */
   1339     private synchronized void releaseView(View view) {
   1340         mActionPool.offer(view);
   1341         mActionRecycled++;
   1342     }
   1343 
   1344     /**
   1345      * Inflate the in-track view for the action of the given MIME-type, collapsing duplicate values.
   1346      * Will use the icon provided by the {@link DataKind}.
   1347      */
   1348     private View inflateAction(String mimeType) {
   1349         final CheckableImageView view = (CheckableImageView)obtainView();
   1350         boolean isActionSet = false;
   1351 
   1352         // Add direct intent if single child, otherwise flag for multiple
   1353         ActionList children = mActions.get(mimeType);
   1354         if (children.size() > 1) {
   1355             Collapser.collapseList(children);
   1356         }
   1357         Action firstInfo = children.get(0);
   1358         if (children.size() == 1) {
   1359             view.setTag(firstInfo);
   1360         } else {
   1361             for (Action action : children) {
   1362                 if (action.isPrimary()) {
   1363                     view.setTag(action);
   1364                     isActionSet = true;
   1365                     break;
   1366                 }
   1367             }
   1368             if (!isActionSet) {
   1369                 view.setTag(children);
   1370             }
   1371         }
   1372 
   1373         // Set icon and listen for clicks
   1374         final CharSequence descrip = mResolveCache.getDescription(firstInfo);
   1375         final Drawable icon = mResolveCache.getIcon(firstInfo);
   1376         view.setChecked(false);
   1377         view.setContentDescription(descrip);
   1378         view.setImageDrawable(icon);
   1379         view.setOnClickListener(this);
   1380         return view;
   1381     }
   1382 
   1383     /** {@inheritDoc} */
   1384     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
   1385         // Pass list item clicks along so that Intents are handled uniformly
   1386         onClick(view);
   1387     }
   1388 
   1389     /**
   1390      * Flag indicating if {@link #mArrowDown} was visible during the last call
   1391      * to {@link #setResolveVisible(boolean, CheckableImageView)}. Used to
   1392      * decide during a later call if the arrow should be restored.
   1393      */
   1394     private boolean mWasDownArrow = false;
   1395 
   1396     /**
   1397      * Helper for showing and hiding {@link #mFooterDisambig}, which will
   1398      * correctly manage {@link #mArrowDown} as needed.
   1399      */
   1400     private void setResolveVisible(boolean visible, CheckableImageView actionView) {
   1401         // Show or hide the resolve list if needed
   1402         boolean visibleNow = mFooterDisambig.getVisibility() == View.VISIBLE;
   1403 
   1404         if (mLastAction != null) mLastAction.setChecked(false);
   1405         if (actionView != null) actionView.setChecked(true);
   1406         mLastAction = actionView;
   1407 
   1408         // Bail early if already in desired state
   1409         if (visible == visibleNow) return;
   1410 
   1411         mFooter.setVisibility(visible ? View.GONE : View.VISIBLE);
   1412         mFooterDisambig.setVisibility(visible ? View.VISIBLE : View.GONE);
   1413 
   1414         if (visible) {
   1415             // If showing list, then hide and save state of down arrow
   1416             mWasDownArrow = mWasDownArrow || (mArrowDown.getVisibility() == View.VISIBLE);
   1417             mArrowDown.setVisibility(View.INVISIBLE);
   1418         } else {
   1419             // If hiding list, restore any down arrow state
   1420             mArrowDown.setVisibility(mWasDownArrow ? View.VISIBLE : View.INVISIBLE);
   1421         }
   1422     }
   1423 
   1424     /** {@inheritDoc} */
   1425     public void onClick(View view) {
   1426         final boolean isActionView = (view instanceof CheckableImageView);
   1427         final CheckableImageView actionView = isActionView ? (CheckableImageView)view : null;
   1428         final Object tag = view.getTag();
   1429         if (tag instanceof Action) {
   1430             // Incoming tag is concrete intent, so try launching
   1431             final Action action = (Action)tag;
   1432             final boolean makePrimary = mMakePrimary;
   1433 
   1434             if (Intent.ACTION_CALL_PRIVILEGED.equals(action.getIntent().getAction())) {
   1435                 StickyTabs.saveTab(mContext, mLastSelectedContactsAppTab);
   1436             }
   1437 
   1438             try {
   1439                 mContext.startActivity(action.getIntent());
   1440             } catch (ActivityNotFoundException e) {
   1441                 Toast.makeText(mContext, R.string.quickcontact_missing_app, Toast.LENGTH_SHORT)
   1442                         .show();
   1443             }
   1444 
   1445             // Hide the resolution list, if present
   1446             setResolveVisible(false, null);
   1447             this.dismiss();
   1448 
   1449             if (makePrimary) {
   1450                 ContentValues values = new ContentValues(1);
   1451                 values.put(Data.IS_SUPER_PRIMARY, 1);
   1452                 final Uri dataUri = action.getDataUri();
   1453                 if (dataUri != null) {
   1454                     mContext.getContentResolver().update(dataUri, values, null, null);
   1455                 }
   1456             }
   1457         } else if (tag instanceof ActionList) {
   1458             // Incoming tag is a MIME-type, so show resolution list
   1459             final ActionList children = (ActionList)tag;
   1460 
   1461             // Show resolution list and set adapter
   1462             setResolveVisible(true, actionView);
   1463 
   1464             mResolveList.setOnItemClickListener(this);
   1465             mResolveList.setAdapter(new BaseAdapter() {
   1466                 public int getCount() {
   1467                     return children.size();
   1468                 }
   1469 
   1470                 public Object getItem(int position) {
   1471                     return children.get(position);
   1472                 }
   1473 
   1474                 public long getItemId(int position) {
   1475                     return position;
   1476                 }
   1477 
   1478                 public View getView(int position, View convertView, ViewGroup parent) {
   1479                     if (convertView == null) {
   1480                         convertView = mInflater.inflate(
   1481                                 R.layout.quickcontact_resolve_item, parent, false);
   1482                     }
   1483 
   1484                     // Set action title based on summary value
   1485                     final Action action = (Action)getItem(position);
   1486 
   1487                     TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
   1488                     TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
   1489 
   1490                     text1.setText(action.getHeader());
   1491                     text2.setText(action.getBody());
   1492 
   1493                     convertView.setTag(action);
   1494                     return convertView;
   1495                 }
   1496             });
   1497 
   1498             // Make sure we resize to make room for ListView
   1499             mDecor.forceLayout();
   1500             mDecor.invalidate();
   1501 
   1502         }
   1503     }
   1504 
   1505     public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
   1506         mMakePrimary = isChecked;
   1507     }
   1508 
   1509     private void onBackPressed() {
   1510         // Back key will first dismiss any expanded resolve list, otherwise
   1511         // it will close the entire dialog.
   1512         if (mFooterDisambig.getVisibility() == View.VISIBLE) {
   1513             setResolveVisible(false, null);
   1514             mDecor.forceLayout();
   1515             mDecor.invalidate();
   1516         } else {
   1517             dismiss();
   1518         }
   1519     }
   1520 
   1521     /** {@inheritDoc} */
   1522     public boolean dispatchKeyEvent(KeyEvent event) {
   1523         if (mWindow.superDispatchKeyEvent(event)) {
   1524             return true;
   1525         }
   1526         return event.dispatch(this, mDecor != null
   1527                 ? mDecor.getKeyDispatcherState() : null, this);
   1528     }
   1529 
   1530     /** {@inheritDoc} */
   1531     public boolean onKeyDown(int keyCode, KeyEvent event) {
   1532         if (keyCode == KeyEvent.KEYCODE_BACK) {
   1533             event.startTracking();
   1534             return true;
   1535         }
   1536 
   1537         return false;
   1538     }
   1539 
   1540     /** {@inheritDoc} */
   1541     public boolean onKeyUp(int keyCode, KeyEvent event) {
   1542         if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking()
   1543                 && !event.isCanceled()) {
   1544             onBackPressed();
   1545             return true;
   1546         }
   1547 
   1548         return false;
   1549     }
   1550 
   1551     /** {@inheritDoc} */
   1552     public boolean onKeyLongPress(int keyCode, KeyEvent event) {
   1553         return false;
   1554     }
   1555 
   1556     /** {@inheritDoc} */
   1557     public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
   1558         return false;
   1559     }
   1560 
   1561     /** {@inheritDoc} */
   1562     public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
   1563         // TODO: make this window accessible
   1564         return false;
   1565     }
   1566 
   1567     /**
   1568      * Detect if the given {@link MotionEvent} is outside the boundaries of this
   1569      * window, which usually means we should dismiss.
   1570      */
   1571     protected void detectEventOutside(MotionEvent event) {
   1572         if (event.getAction() == MotionEvent.ACTION_DOWN && mDecor != null) {
   1573             // Only try detecting outside events on down-press
   1574             mDecor.getHitRect(mRect);
   1575             mRect.top = mRect.top + mShadowTouch;
   1576             mRect.bottom = mRect.bottom - mShadowTouch;
   1577             final int x = (int)event.getX();
   1578             final int y = (int)event.getY();
   1579             if (!mRect.contains(x, y)) {
   1580                 event.setAction(MotionEvent.ACTION_OUTSIDE);
   1581             }
   1582         }
   1583     }
   1584 
   1585     /** {@inheritDoc} */
   1586     public boolean dispatchTouchEvent(MotionEvent event) {
   1587         detectEventOutside(event);
   1588         if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
   1589             dismiss();
   1590             return true;
   1591         } else {
   1592             return mWindow.superDispatchTouchEvent(event);
   1593         }
   1594     }
   1595 
   1596     /** {@inheritDoc} */
   1597     public boolean dispatchTrackballEvent(MotionEvent event) {
   1598         return mWindow.superDispatchTrackballEvent(event);
   1599     }
   1600 
   1601     /** {@inheritDoc} */
   1602     public void onContentChanged() {
   1603     }
   1604 
   1605     /** {@inheritDoc} */
   1606     public boolean onCreatePanelMenu(int featureId, Menu menu) {
   1607         return false;
   1608     }
   1609 
   1610     /** {@inheritDoc} */
   1611     public View onCreatePanelView(int featureId) {
   1612         return null;
   1613     }
   1614 
   1615     /** {@inheritDoc} */
   1616     public boolean onMenuItemSelected(int featureId, MenuItem item) {
   1617         return false;
   1618     }
   1619 
   1620     /** {@inheritDoc} */
   1621     public boolean onMenuOpened(int featureId, Menu menu) {
   1622         return false;
   1623     }
   1624 
   1625     /** {@inheritDoc} */
   1626     public void onPanelClosed(int featureId, Menu menu) {
   1627     }
   1628 
   1629     /** {@inheritDoc} */
   1630     public boolean onPreparePanel(int featureId, View view, Menu menu) {
   1631         return false;
   1632     }
   1633 
   1634     /** {@inheritDoc} */
   1635     public boolean onSearchRequested() {
   1636         return false;
   1637     }
   1638 
   1639     /** {@inheritDoc} */
   1640     public void onWindowAttributesChanged(android.view.WindowManager.LayoutParams attrs) {
   1641         if (mDecor != null) {
   1642             mWindowManager.updateViewLayout(mDecor, attrs);
   1643         }
   1644     }
   1645 
   1646     /** {@inheritDoc} */
   1647     public void onWindowFocusChanged(boolean hasFocus) {
   1648     }
   1649 
   1650     /** {@inheritDoc} */
   1651     public void onAttachedToWindow() {
   1652         // No actions
   1653     }
   1654 
   1655     /** {@inheritDoc} */
   1656     public void onDetachedFromWindow() {
   1657         // No actions
   1658     }
   1659 
   1660     private interface DataQuery {
   1661         final String[] PROJECTION = new String[] {
   1662                 Data._ID,
   1663 
   1664                 RawContacts.ACCOUNT_TYPE,
   1665                 Contacts.STARRED,
   1666                 Contacts.DISPLAY_NAME,
   1667                 Contacts.CONTACT_PRESENCE,
   1668 
   1669                 Data.STATUS,
   1670                 Data.STATUS_RES_PACKAGE,
   1671                 Data.STATUS_ICON,
   1672                 Data.STATUS_LABEL,
   1673                 Data.STATUS_TIMESTAMP,
   1674                 Data.PRESENCE,
   1675 
   1676                 Data.RES_PACKAGE,
   1677                 Data.MIMETYPE,
   1678                 Data.IS_PRIMARY,
   1679                 Data.IS_SUPER_PRIMARY,
   1680                 Data.RAW_CONTACT_ID,
   1681 
   1682                 Data.DATA1, Data.DATA2, Data.DATA3, Data.DATA4, Data.DATA5,
   1683                 Data.DATA6, Data.DATA7, Data.DATA8, Data.DATA9, Data.DATA10, Data.DATA11,
   1684                 Data.DATA12, Data.DATA13, Data.DATA14, Data.DATA15,
   1685         };
   1686 
   1687         final int _ID = 0;
   1688 
   1689         final int ACCOUNT_TYPE = 1;
   1690         final int STARRED = 2;
   1691         final int DISPLAY_NAME = 3;
   1692         final int CONTACT_PRESENCE = 4;
   1693 
   1694         final int STATUS = 5;
   1695         final int STATUS_RES_PACKAGE = 6;
   1696         final int STATUS_ICON = 7;
   1697         final int STATUS_LABEL = 8;
   1698         final int STATUS_TIMESTAMP = 9;
   1699         final int PRESENCE = 10;
   1700 
   1701         final int RES_PACKAGE = 11;
   1702         final int MIMETYPE = 12;
   1703         final int IS_PRIMARY = 13;
   1704         final int IS_SUPER_PRIMARY = 14;
   1705     }
   1706 }
   1707