OSDN Git Service

Follow-up on preventing QC from hiding soft keyboard.
[android-x86/packages-apps-Contacts.git] / src / com / android / contacts / ui / QuickContactWindow.java
index 747c204..6d4ff9b 100644 (file)
@@ -16,6 +16,8 @@
 
 package com.android.contacts.ui;
 
+import com.android.contacts.Collapser;
+import com.android.contacts.ContactPresenceIconUtil;
 import com.android.contacts.ContactsUtils;
 import com.android.contacts.R;
 import com.android.contacts.model.ContactsSource;
@@ -23,17 +25,17 @@ import com.android.contacts.model.Sources;
 import com.android.contacts.model.ContactsSource.DataKind;
 import com.android.contacts.ui.widget.CheckableImageView;
 import com.android.contacts.util.Constants;
+import com.android.contacts.util.DataStatus;
 import com.android.contacts.util.NotifyingAsyncQueryHandler;
 import com.android.internal.policy.PolicyManager;
-import com.google.android.collect.Lists;
 import com.google.android.collect.Sets;
 
 import android.content.ActivityNotFoundException;
-import android.content.ContentValues;
 import android.content.ContentUris;
+import android.content.ContentValues;
 import android.content.Context;
-import android.content.EntityIterator;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
@@ -44,7 +46,6 @@ import android.graphics.BitmapFactory;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
-import android.os.Handler;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.QuickContact;
@@ -54,14 +55,11 @@ import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.Im;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.Photo;
-import android.provider.SocialContract.Activities;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
 import android.text.TextUtils;
-import android.text.format.DateUtils;
+import android.util.AttributeSet;
 import android.util.Log;
-import android.util.Pool;
-import android.util.Poolable;
-import android.util.PoolableManager;
-import android.util.Pools;
 import android.view.ContextThemeWrapper;
 import android.view.Gravity;
 import android.view.KeyEvent;
@@ -69,13 +67,12 @@ import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.MotionEvent;
-import android.view.VelocityTracker;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewStub;
 import android.view.Window;
 import android.view.WindowManager;
-import android.view.Window.Callback;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.animation.Animation;
 import android.view.animation.AnimationUtils;
@@ -88,6 +85,7 @@ import android.widget.CompoundButton;
 import android.widget.HorizontalScrollView;
 import android.widget.ImageView;
 import android.widget.ListView;
+import android.widget.RelativeLayout;
 import android.widget.TextView;
 import android.widget.Toast;
 
@@ -105,7 +103,8 @@ import java.util.Set;
  */
 public class QuickContactWindow implements Window.Callback,
         NotifyingAsyncQueryHandler.AsyncQueryListener, View.OnClickListener,
-        AbsListView.OnItemClickListener, CompoundButton.OnCheckedChangeListener, KeyEvent.Callback {
+        AbsListView.OnItemClickListener, CompoundButton.OnCheckedChangeListener, KeyEvent.Callback,
+        OnGlobalLayoutListener {
     private static final String TAG = "QuickContactWindow";
 
     /**
@@ -116,6 +115,32 @@ public class QuickContactWindow implements Window.Callback,
         public void onDismiss(QuickContactWindow dialog);
     }
 
+    /**
+     * Custom layout the sole purpose of which is to intercept the BACK key and
+     * close QC even when the soft keyboard is open.
+     */
+    public static class RootLayout extends RelativeLayout {
+
+        QuickContactWindow mQuickContactWindow;
+
+        public RootLayout(Context context, AttributeSet attrs) {
+            super(context, attrs);
+        }
+
+        /**
+         * Intercepts the BACK key event and dismisses QuickContact window.
+         */
+        @Override
+        public boolean dispatchKeyEventPreIme(KeyEvent event) {
+            if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
+                mQuickContactWindow.onBackPressed();
+                return true;
+            } else {
+                return super.dispatchKeyEventPreIme(event);
+            }
+        }
+    }
+
     private final Context mContext;
     private final LayoutInflater mInflater;
     private final WindowManager mWindowManager;
@@ -123,6 +148,7 @@ public class QuickContactWindow implements Window.Callback,
     private View mDecor;
     private final Rect mRect = new Rect();
 
+    private boolean mDismissed = false;
     private boolean mQuerying = false;
     private boolean mShowing = false;
 
@@ -133,7 +159,13 @@ public class QuickContactWindow implements Window.Callback,
     private Uri mLookupUri;
     private Rect mAnchor;
 
-    private int mShadowHeight;
+    private int mShadowHoriz;
+    private int mShadowVert;
+    private int mShadowTouch;
+
+    private int mScreenWidth;
+    private int mScreenHeight;
+    private int mRequestedY;
 
     private boolean mHasValidSocial = false;
     private boolean mHasData = false;
@@ -143,6 +175,7 @@ public class QuickContactWindow implements Window.Callback,
     private ImageView mArrowDown;
 
     private int mMode;
+    private RootLayout mRootView;
     private View mHeader;
     private HorizontalScrollView mTrackScroll;
     private ViewGroup mTrack;
@@ -173,10 +206,17 @@ public class QuickContactWindow implements Window.Callback,
     private String[] mExcludeMimes;
 
     /**
-     * Specific MIME-types that should be bumped to the front of the dialog.
-     * Other MIME-types not appearing in this list follow in alphabetic order.
+     * {@link #PRECEDING_MIMETYPES} and {@link #FOLLOWING_MIMETYPES} are used to sort MIME-types.
+     *
+     * <p>The MIME-types in {@link #PRECEDING_MIMETYPES} appear in the front of the dialog,
+     * in the order in the array.
+     *
+     * <p>The ones in {@link #FOLLOWING_MIMETYPES} appear in the end of the dialog, in alphabetical
+     * order.
+     *
+     * <p>The rest go between them, in the order in the array.
      */
-    private static final String[] ORDERED_MIMETYPES = new String[] {
+    private static final String[] PRECEDING_MIMETYPES = new String[] {
             Phone.CONTENT_ITEM_TYPE,
             Contacts.CONTENT_ITEM_TYPE,
             Constants.MIME_SMS_ADDRESS,
@@ -184,19 +224,27 @@ public class QuickContactWindow implements Window.Callback,
     };
 
     /**
+     * See {@link #PRECEDING_MIMETYPES}.
+     */
+    private static final String[] FOLLOWING_MIMETYPES = new String[] {
+            StructuredPostal.CONTENT_ITEM_TYPE,
+            Website.CONTENT_ITEM_TYPE,
+    };
+
+    /**
      * Specific list {@link ApplicationInfo#packageName} of apps that are
      * prefered <strong>only</strong> for the purposes of default icons when
      * multiple {@link ResolveInfo} are found to match. This only happens when
      * the user has not selected a default app yet, and they will still be
      * presented with the system disambiguation dialog.
      */
-    private static final HashSet<String> sPreferResolve = Sets.newHashSet(new String[] {
+    private static final HashSet<String> sPreferResolve = Sets.newHashSet(
             "com.android.email",
             "com.android.calendar",
             "com.android.contacts",
             "com.android.mms",
             "com.android.phone",
-    });
+            "com.android.browser");
 
     private static final int TOKEN_DATA = 1;
 
@@ -216,16 +264,28 @@ public class QuickContactWindow implements Window.Callback,
         mWindow = PolicyManager.makeNewWindow(mContext);
         mWindow.setCallback(this);
         mWindow.setWindowManager(mWindowManager, null, null);
+        mWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED);
 
         mWindow.setContentView(R.layout.quickcontact);
 
+        mRootView = (RootLayout)mWindow.findViewById(R.id.root);
+        mRootView.mQuickContactWindow = this;
+        mRootView.setFocusable(true);
+        mRootView.setFocusableInTouchMode(true);
+        mRootView.setDescendantFocusability(RootLayout.FOCUS_AFTER_DESCENDANTS);
+
         mArrowUp = (ImageView)mWindow.findViewById(R.id.arrow_up);
         mArrowDown = (ImageView)mWindow.findViewById(R.id.arrow_down);
 
         mResolveCache = new ResolveCache(mContext);
 
         final Resources res = mContext.getResources();
-        mShadowHeight = res.getDimensionPixelSize(R.dimen.quickcontact_shadow);
+        mShadowHoriz = res.getDimensionPixelSize(R.dimen.quickcontact_shadow_horiz);
+        mShadowVert = res.getDimensionPixelSize(R.dimen.quickcontact_shadow_vert);
+        mShadowTouch = res.getDimensionPixelSize(R.dimen.quickcontact_shadow_touch);
+
+        mScreenWidth = mWindowManager.getDefaultDisplay().getWidth();
+        mScreenHeight = mWindowManager.getDefaultDisplay().getHeight();
 
         mTrack = (ViewGroup)mWindow.findViewById(R.id.quickcontact);
         mTrackScroll = (HorizontalScrollView)mWindow.findViewById(R.id.scroll);
@@ -278,7 +338,7 @@ public class QuickContactWindow implements Window.Callback,
             // Inflate actual header if we picked a stub
             final ViewStub stub = (ViewStub)header;
             header = stub.inflate();
-        } else {
+        } else if (header != null) {
             header.setVisibility(View.VISIBLE);
         }
 
@@ -289,16 +349,28 @@ public class QuickContactWindow implements Window.Callback,
      * Start showing a dialog for the given {@link Contacts#_ID} pointing
      * towards the given location.
      */
-    public void show(Uri lookupUri, Rect anchor, int mode, String[] excludeMimes) {
-        if (mShowing || mQuerying) {
-            Log.w(TAG, "already in process of showing");
-            return;
+    public synchronized void show(Uri lookupUri, Rect anchor, int mode, String[] excludeMimes) {
+        if (mQuerying || mShowing) {
+            Log.w(TAG, "dismissing before showing");
+            dismissInternal();
         }
 
         if (TRACE_LAUNCH && !android.os.Debug.isMethodTracingActive()) {
             android.os.Debug.startMethodTracing(TRACE_TAG);
         }
 
+        // Validate incoming parameters
+        final boolean validMode = (mode == QuickContact.MODE_SMALL
+                || mode == QuickContact.MODE_MEDIUM || mode == QuickContact.MODE_LARGE);
+        if (!validMode) {
+            throw new IllegalArgumentException("Invalid mode, expecting MODE_LARGE, "
+                    + "MODE_MEDIUM, or MODE_SMALL");
+        }
+
+        if (anchor == null) {
+            throw new IllegalArgumentException("Missing anchor rectangle");
+        }
+
         // Prepare header view for requested mode
         mLookupUri = lookupUri;
         mAnchor = new Rect(anchor);
@@ -313,9 +385,15 @@ public class QuickContactWindow implements Window.Callback,
         setHeaderText(R.id.timestamp, null);
 
         setHeaderImage(R.id.presence, null);
-        setHeaderImage(R.id.source, null);
+
+        resetTrack();
+
+        // We need to have a focused view inside the QuickContact window so
+        // that the BACK key event can be intercepted
+        mRootView.requestFocus();
 
         mHasValidSocial = false;
+        mDismissed = false;
         mQuerying = true;
 
         // Start background query for data, but only select photo rows when they
@@ -326,12 +404,12 @@ public class QuickContactWindow implements Window.Callback,
         // Only request photo data when required by mode
         if (mMode == QuickContact.MODE_LARGE) {
             // Select photos, but only super-primary
-            mHandler.startQuery(TOKEN_DATA, null, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
+            mHandler.startQuery(TOKEN_DATA, lookupUri, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
                     + "!=? OR (" + Data.MIMETYPE + "=? AND " + Data._ID + "=" + Contacts.PHOTO_ID
                     + ")", new String[] { Photo.CONTENT_ITEM_TYPE, Photo.CONTENT_ITEM_TYPE }, null);
         } else {
             // Exclude all photos from cursor
-            mHandler.startQuery(TOKEN_DATA, null, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
+            mHandler.startQuery(TOKEN_DATA, lookupUri, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
                     + "!=?", new String[] { Photo.CONTENT_ITEM_TYPE }, null);
         }
     }
@@ -379,41 +457,43 @@ public class QuickContactWindow implements Window.Callback,
      */
     private void showInternal() {
         mDecor = mWindow.getDecorView();
+        mDecor.getViewTreeObserver().addOnGlobalLayoutListener(this);
         WindowManager.LayoutParams l = mWindow.getAttributes();
 
-        l.width = WindowManager.LayoutParams.FILL_PARENT;
+        l.width = mScreenWidth + mShadowHoriz + mShadowHoriz;
         l.height = WindowManager.LayoutParams.WRAP_CONTENT;
 
         // Force layout measuring pass so we have baseline numbers
         mDecor.measure(l.width, l.height);
-
         final int blockHeight = mDecor.getMeasuredHeight();
 
         l.gravity = Gravity.TOP | Gravity.LEFT;
-        l.x = 0;
+        l.x = -mShadowHoriz;
 
         if (mAnchor.top > blockHeight) {
             // Show downwards callout when enough room, aligning bottom block
             // edge with top of anchor area, and adjusting to inset arrow.
             showArrow(R.id.arrow_down, mAnchor.centerX());
-            l.y = mAnchor.top - blockHeight + mShadowHeight;
+            l.y = mAnchor.top - blockHeight + mShadowVert;
             l.windowAnimations = R.style.QuickContactAboveAnimation;
 
         } else {
             // Otherwise show upwards callout, aligning block top with bottom of
             // anchor area, and adjusting to inset arrow.
             showArrow(R.id.arrow_up, mAnchor.centerX());
-            l.y = mAnchor.bottom - mShadowHeight;
+            l.y = mAnchor.bottom - mShadowVert;
             l.windowAnimations = R.style.QuickContactBelowAnimation;
 
         }
 
         l.flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
-                | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
+                | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
 
+        mRequestedY = l.y;
         mWindowManager.addView(mDecor, l);
         mShowing = true;
         mQuerying = false;
+        mDismissed = false;
 
         mTrack.startAnimation(mTrackAnim);
 
@@ -424,35 +504,75 @@ public class QuickContactWindow implements Window.Callback,
         }
     }
 
+    /** {@inheritDoc} */
+    public void onGlobalLayout() {
+        layoutInScreen();
+    }
+
+    /**
+     * Adjust vertical {@link WindowManager.LayoutParams} to fit window as best
+     * as possible, shifting up to display content as needed.
+     */
+    private void layoutInScreen() {
+        if (!mShowing) return;
+
+        final WindowManager.LayoutParams l = mWindow.getAttributes();
+        final int originalY = l.y;
+
+        final int blockHeight = mDecor.getHeight();
+
+        l.y = mRequestedY;
+        if (mRequestedY + blockHeight > mScreenHeight) {
+            // Shift up from bottom when overflowing
+            l.y = mScreenHeight - blockHeight;
+        }
+
+        if (originalY != l.y) {
+            // Only update when value is changed
+            mWindow.setAttributes(l);
+        }
+    }
+
     /**
      * Dismiss this dialog if showing.
      */
-    public void dismiss() {
+    public synchronized void dismiss() {
         // Notify any listeners that we've been dismissed
         if (mDismissListener != null) {
             mDismissListener.onDismiss(this);
         }
 
-        if (!isShowing()) {
-            if (LOGD) Log.d(TAG, "not visible, ignore");
-            return;
-        }
+        dismissInternal();
+    }
 
+    private void dismissInternal() {
+        // Remove any attached window decor for recycling
         boolean hadDecor = mDecor != null;
         if (hadDecor) {
             mWindowManager.removeView(mDecor);
+            mWindowRecycled++;
+            mDecor.getViewTreeObserver().removeGlobalOnLayoutListener(this);
             mDecor = null;
             mWindow.closeAllPanels();
         }
+        mShowing = false;
+        mDismissed = true;
 
-        // Release reference to last chiclet.
-        mLastAction = null;
+        // Cancel any pending queries
+        mHandler.cancelOperation(TOKEN_DATA);
+        mQuerying = false;
 
-        // Completely hide header from current mode
+        // Completely hide header and reset track
         mHeader.setVisibility(View.GONE);
+        resetTrack();
+    }
 
-        // Cancel any pending queries
-        mHandler.cancelOperation(TOKEN_DATA);
+    /**
+     * Reset track to initial state, recycling any chiclets.
+     */
+    private void resetTrack() {
+        // Release reference to last chiclet
+        mLastAction = null;
 
         // Clear track actions and scroll to hard left
         mResolveCache.clear();
@@ -467,32 +587,19 @@ public class QuickContactWindow implements Window.Callback,
         mTrackScroll.fullScroll(View.FOCUS_LEFT);
         mWasDownArrow = false;
 
-        setResolveVisible(false, null);
-
-        mQuerying = false;
-
-        if (!hadDecor || !mShowing) {
-            if (LOGD) Log.d(TAG, "not showing, ignore");
-            return;
-        }
-
-        mShowing = false;
-        mWindowRecycled++;
-    }
+        // Clear any primary requests
+        mMakePrimary = false;
+        mSetPrimaryCheckBox.setChecked(false);
 
-    /**
-     * Returns true if this dialog is showing or querying.
-     */
-    public boolean isShowing() {
-        return mShowing || mQuerying;
+        setResolveVisible(false, null);
     }
 
     /**
      * Consider showing this window, which will only call through to
      * {@link #showInternal()} when all data items are present.
      */
-    private synchronized void considerShowing() {
-        if (mHasData && !mShowing) {
+    private void considerShowing() {
+        if (mHasData && !mShowing && !mDismissed) {
             if (mMode == QuickContact.MODE_MEDIUM && !mHasValidSocial) {
                 // Missing valid social, swap medium for small header
                 mHeader.setVisibility(View.GONE);
@@ -505,7 +612,10 @@ public class QuickContactWindow implements Window.Callback,
     }
 
     /** {@inheritDoc} */
-    public void onQueryComplete(int token, Object cookie, Cursor cursor) {
+    public synchronized void onQueryComplete(int token, Object cookie, Cursor cursor) {
+        // Bail early when query is stale
+        if (cookie != mLookupUri) return;
+
         if (cursor == null) {
             // Problem while running query, so bail without showing
             Log.w(TAG, "Missing cursor for token=" + token);
@@ -538,11 +648,6 @@ public class QuickContactWindow implements Window.Callback,
     }
 
     /** Assign this image to the view, if found in {@link #mHeader}. */
-    private void setHeaderImage(int id, int resId) {
-        setHeaderImage(id, mContext.getResources().getDrawable(resId));
-    }
-
-    /** Assign this image to the view, if found in {@link #mHeader}. */
     private void setHeaderImage(int id, Drawable drawable) {
         final View view = mHeader.findViewById(id);
         if (view instanceof ImageView) {
@@ -552,34 +657,10 @@ public class QuickContactWindow implements Window.Callback,
     }
 
     /**
-     * Find the presence icon for showing in summary header.
-     */
-    private Drawable getPresenceIcon(int status) {
-        int resId = -1;
-        switch (status) {
-            case StatusUpdates.AVAILABLE:
-                resId = android.R.drawable.presence_online;
-                break;
-            case StatusUpdates.IDLE:
-            case StatusUpdates.AWAY:
-                resId = android.R.drawable.presence_away;
-                break;
-            case StatusUpdates.DO_NOT_DISTURB:
-                resId = android.R.drawable.presence_busy;
-                break;
-        }
-        if (resId != -1) {
-            return mContext.getResources().getDrawable(resId);
-        } else {
-            return null;
-        }
-    }
-
-    /**
      * Find the QuickContact-specific presence icon for showing in chiclets.
      */
     private Drawable getTrackPresenceIcon(int status) {
-        int resId = -1;
+        int resId;
         switch (status) {
             case StatusUpdates.AVAILABLE:
                 resId = R.drawable.quickcontact_slider_presence_active;
@@ -617,7 +698,7 @@ public class QuickContactWindow implements Window.Callback,
      * Abstract definition of an action that could be performed, along with
      * string description and icon.
      */
-    private interface Action {
+    private interface Action extends Collapser.Collapsible<Action> {
         public CharSequence getHeader();
         public CharSequence getBody();
 
@@ -706,24 +787,39 @@ public class QuickContactWindow implements Window.Callback,
                     mIntent = new Intent(Intent.ACTION_SENDTO, mailUri);
                 }
 
+            } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                final String url = getAsString(cursor, Website.URL);
+                if (!TextUtils.isEmpty(url)) {
+                    mIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+                }
+
             } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType)) {
                 final boolean isEmail = Email.CONTENT_ITEM_TYPE.equals(
                         getAsString(cursor, Data.MIMETYPE));
-                final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK :
-                        getAsInt(cursor, Im.PROTOCOL);
-
-                String host = getAsString(cursor, Im.CUSTOM_PROTOCOL);
-                String data = getAsString(cursor, isEmail ? Email.DATA : Im.DATA);
-                if (protocol != Im.PROTOCOL_CUSTOM) {
-                    // Try bringing in a well-known host for specific protocols
-                    host = ContactsUtils.lookupProviderNameFromId(protocol);
-                }
+                if (isEmail || isProtocolValid(cursor)) {
+                    final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK :
+                            getAsInt(cursor, Im.PROTOCOL);
+
+                    if (isEmail) {
+                        // Use Google Talk string when using Email, and clear data
+                        // Uri so we don't try saving Email as primary.
+                        mHeader = context.getText(R.string.chat_gtalk);
+                        mDataUri = null;
+                    }
 
-                if (!TextUtils.isEmpty(host) && !TextUtils.isEmpty(data)) {
-                    final String authority = host.toLowerCase();
-                    final Uri imUri = new Uri.Builder().scheme(Constants.SCHEME_IMTO).authority(
-                            authority).appendPath(data).build();
-                    mIntent = new Intent(Intent.ACTION_SENDTO, imUri);
+                    String host = getAsString(cursor, Im.CUSTOM_PROTOCOL);
+                    String data = getAsString(cursor, isEmail ? Email.DATA : Im.DATA);
+                    if (protocol != Im.PROTOCOL_CUSTOM) {
+                        // Try bringing in a well-known host for specific protocols
+                        host = ContactsUtils.lookupProviderNameFromId(protocol);
+                    }
+
+                    if (!TextUtils.isEmpty(host) && !TextUtils.isEmpty(data)) {
+                        final String authority = host.toLowerCase();
+                        final Uri imUri = new Uri.Builder().scheme(Constants.SCHEME_IMTO).authority(
+                                authority).appendPath(data).build();
+                        mIntent = new Intent(Intent.ACTION_SENDTO, imUri);
+                    }
                 }
             }
 
@@ -733,7 +829,20 @@ public class QuickContactWindow implements Window.Callback,
             }
 
             // Always launch as new task, since we're like a launcher
-            mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        }
+
+        private boolean isProtocolValid(Cursor cursor) {
+            final int columnIndex = cursor.getColumnIndex(Im.PROTOCOL);
+            if (cursor.isNull(columnIndex)) {
+                return false;
+            }
+            try {
+                Integer.valueOf(cursor.getString(columnIndex));
+            } catch (NumberFormatException e) {
+                return false;
+            }
+            return true;
         }
 
         /** {@inheritDoc} */
@@ -781,6 +890,39 @@ public class QuickContactWindow implements Window.Callback,
         public Intent getIntent() {
             return mIntent;
         }
+
+        /** {@inheritDoc} */
+        public boolean collapseWith(Action other) {
+            if (!shouldCollapseWith(other)) {
+                return false;
+            }
+            return true;
+        }
+
+        /** {@inheritDoc} */
+        public boolean shouldCollapseWith(Action t) {
+            if (t == null) {
+                return false;
+            }
+            if (!(t instanceof DataAction)) {
+                Log.e(TAG, "t must be DataAction");
+                return false;
+            }
+            DataAction other = (DataAction)t;
+            if (!ContactsUtils.areObjectsEqual(mKind, other.mKind)) {
+                return false;
+            }
+            if (!ContactsUtils.areDataEqual(mContext, mMimeType, mBody, other.mMimeType,
+                    other.mBody)) {
+                return false;
+            }
+            if (!TextUtils.equals(mMimeType, other.mMimeType)
+                    || !ContactsUtils.areIntentActionEqual(mIntent, other.mIntent)
+                    ) {
+                return false;
+            }
+            return true;
+        }
     }
 
     /**
@@ -818,7 +960,7 @@ public class QuickContactWindow implements Window.Callback,
         /** {@inheritDoc} */
         public Intent getIntent() {
             final Intent intent = new Intent(Intent.ACTION_VIEW, mLookupUri);
-           intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+           intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
            return intent;
         }
 
@@ -832,6 +974,15 @@ public class QuickContactWindow implements Window.Callback,
             return null;
         }
 
+        /** {@inheritDoc} */
+        public boolean collapseWith(Action t) {
+            return false; // Never dup.
+        }
+
+        /** {@inheritDoc} */
+        public boolean shouldCollapseWith(Action t) {
+            return false; // Never dup.
+        }
     }
 
     /**
@@ -839,7 +990,6 @@ public class QuickContactWindow implements Window.Callback,
      * queries, keyed internally on MIME-type.
      */
     private static class ResolveCache {
-        private Context mContext;
         private PackageManager mPackageManager;
 
         /**
@@ -854,7 +1004,6 @@ public class QuickContactWindow implements Window.Callback,
         private HashMap<String, Entry> mCache = new HashMap<String, Entry>();
 
         public ResolveCache(Context context) {
-            mContext = context;
             mPackageManager = context.getPackageManager();
         }
 
@@ -879,7 +1028,7 @@ public class QuickContactWindow implements Window.Callback,
                 if (size == 1) {
                     bestResolve = matches.get(0);
                 } else if (size > 1) {
-                    bestResolve = getBestResolve(matches);
+                    bestResolve = getBestResolve(intent, matches);
                 }
 
                 if (bestResolve != null) {
@@ -903,7 +1052,18 @@ public class QuickContactWindow implements Window.Callback,
          * displaying in the track, and does not shortcut the system
          * {@link Intent} disambiguation dialog.
          */
-        protected ResolveInfo getBestResolve(List<ResolveInfo> matches) {
+        protected ResolveInfo getBestResolve(Intent intent, List<ResolveInfo> matches) {
+            // Try finding preferred activity, otherwise detect disambig
+            final ResolveInfo foundResolve = mPackageManager.resolveActivity(intent,
+                    PackageManager.MATCH_DEFAULT_ONLY);
+            final boolean foundDisambig = (foundResolve.match &
+                    IntentFilter.MATCH_CATEGORY_MASK) == 0;
+
+            if (!foundDisambig) {
+                // Found concrete match, so return directly
+                return foundResolve;
+            }
+
             // Accept any package from prefer list, otherwise first system app
             ResolveInfo firstSystem = null;
             for (ResolveInfo info : matches) {
@@ -912,6 +1072,8 @@ public class QuickContactWindow implements Window.Callback,
                 final boolean isPrefer = QuickContactWindow.sPreferResolve
                         .contains(info.activityInfo.applicationInfo.packageName);
 
+
+
                 if (isPrefer) return info;
                 if (isSystem && firstSystem != null) firstSystem = info;
             }
@@ -963,7 +1125,7 @@ public class QuickContactWindow implements Window.Callback,
      * Provide a strongly-typed {@link LinkedList} that holds a list of
      * {@link Action} objects.
      */
-    private class ActionList extends LinkedList<Action> {
+    private class ActionList extends ArrayList<Action> {
     }
 
     /**
@@ -997,99 +1159,6 @@ public class QuickContactWindow implements Window.Callback,
     }
 
     /**
-     * Internal storage for the latest social status, as built when walking
-     * across a {@Link DataQuery} query. Will always keep record of at
-     * least the first status it encounters, but will replace it with newer
-     * statuses, as determined by timestamps.
-     */
-    private static class LatestStatus {
-        private String mStatus = null;
-        private long mTimestamp = -1;
-
-        private String mResPackage = null;
-        private int mIconRes = -1;
-        private int mLabelRes = -1;
-
-        private int getCursorInt(Cursor cursor, int columnIndex, int missingValue) {
-            if (cursor.isNull(columnIndex)) return missingValue;
-            return cursor.getInt(columnIndex);
-        }
-
-        /**
-         * Attempt updating this {@link LatestStatus} based on values at the
-         * current row of the given {@link Cursor}. Assumes that query
-         * projection was {@link DataQuery#PROJECTION}.
-         */
-        public void possibleUpdate(Cursor cursor) {
-            final boolean hasStatus = !cursor.isNull(DataQuery.STATUS);
-            final boolean hasTimestamp = !cursor.isNull(DataQuery.STATUS_TIMESTAMP);
-
-            // Bail early when not valid status, or when previous status was
-            // found and we can't compare this one.
-            if (!hasStatus) return;
-            if (isValid() && !hasTimestamp) return;
-
-            if (hasTimestamp) {
-                // Compare timestamps and bail if older status
-                final long newTimestamp = cursor.getLong(DataQuery.STATUS_TIMESTAMP);
-                if (newTimestamp < mTimestamp) return;
-
-                mTimestamp = newTimestamp;
-            }
-
-            // Fill in remaining details from cursor
-            mStatus = cursor.getString(DataQuery.STATUS);
-            mResPackage = cursor.getString(DataQuery.STATUS_RES_PACKAGE);
-            mIconRes = getCursorInt(cursor, DataQuery.STATUS_ICON, -1);
-            mLabelRes = getCursorInt(cursor, DataQuery.STATUS_LABEL, -1);
-        }
-
-        public boolean isValid() {
-            return !TextUtils.isEmpty(mStatus);
-        }
-
-        public CharSequence getStatus() {
-            return mStatus;
-        }
-
-        /**
-         * Build any timestamp and label into a single string.
-         */
-        public CharSequence getTimestampLabel(Context context) {
-            final PackageManager pm = context.getPackageManager();
-
-            final boolean validTimestamp = mTimestamp > 0;
-            final boolean validLabel = mResPackage != null && mLabelRes != -1;
-
-            final CharSequence timeClause = validTimestamp ? DateUtils.getRelativeTimeSpanString(
-                    mTimestamp, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS,
-                    DateUtils.FORMAT_ABBREV_RELATIVE) : null;
-            final CharSequence labelClause = validLabel ? pm.getText(mResPackage, mLabelRes,
-                    null) : null;
-
-            if (validTimestamp && validLabel) {
-                return context.getString(
-                        com.android.internal.R.string.contact_status_update_attribution_with_date,
-                        timeClause, labelClause);
-            } else if (validLabel) {
-                return context.getString(
-                        com.android.internal.R.string.contact_status_update_attribution,
-                        labelClause);
-            } else if (validTimestamp) {
-                return timeClause;
-            } else {
-                return null;
-            }
-        }
-
-        public Drawable getIcon(Context context) {
-            final PackageManager pm = context.getPackageManager();
-            final boolean validIcon = mResPackage != null && mIconRes != -1;
-            return validIcon ? pm.getDrawable(mResPackage, mIconRes, null) : null;
-        }
-    }
-
-    /**
      * Handle the result from the {@link #TOKEN_DATA} query.
      */
     private void handleData(Cursor cursor) {
@@ -1101,7 +1170,7 @@ public class QuickContactWindow implements Window.Callback,
             mActions.collect(Contacts.CONTENT_ITEM_TYPE, action);
         }
 
-        final LatestStatus status = new LatestStatus();
+        final DataStatus status = new DataStatus();
         final Sources sources = Sources.getInstance(mContext);
         final ImageView photoView = (ImageView)mHeader.findViewById(R.id.photo);
 
@@ -1109,7 +1178,6 @@ public class QuickContactWindow implements Window.Callback,
         while (cursor.moveToNext()) {
             final long dataId = cursor.getLong(DataQuery._ID);
             final String accountType = cursor.getString(DataQuery.ACCOUNT_TYPE);
-            final String resPackage = cursor.getString(DataQuery.RES_PACKAGE);
             final String mimeType = cursor.getString(DataQuery.MIMETYPE);
 
             // Handle any social status updates from this row
@@ -1163,7 +1231,7 @@ public class QuickContactWindow implements Window.Callback,
             // Read contact information from last data row
             final String name = cursor.getString(DataQuery.DISPLAY_NAME);
             final int presence = cursor.getInt(DataQuery.CONTACT_PRESENCE);
-            final Drawable statusIcon = getPresenceIcon(presence);
+            final Drawable statusIcon = ContactPresenceIconUtil.getPresenceIcon(mContext, presence);
 
             setHeaderText(R.id.name, name);
             setHeaderImage(R.id.presence, statusIcon);
@@ -1180,34 +1248,41 @@ public class QuickContactWindow implements Window.Callback,
             // Update status when valid was found
             setHeaderText(R.id.status, status.getStatus());
             setHeaderText(R.id.timestamp, status.getTimestampLabel(mContext));
+        }
+
+        // Turn our list of actions into UI elements
 
-            final Drawable icon = status.getIcon(mContext);
-            setHeaderImage(R.id.source, icon);
+        // Index where we start adding child views.
+        int index = mTrack.getChildCount() - 1;
 
-            if (mMode == QuickContact.MODE_MEDIUM) {
-                // Hide medium divider when missing icon
-                final boolean validIcon = icon != null;
-                mHeader.findViewById(R.id.source_divider).setVisibility(
-                        validIcon ? View.VISIBLE : View.GONE);
+        // All the mime-types to add.
+        final Set<String> containedTypes = new HashSet<String>(mActions.keySet());
+
+        // First, add PRECEDING_MIMETYPES, which are most common.
+        for (String mimeType : PRECEDING_MIMETYPES) {
+            if (containedTypes.contains(mimeType)) {
+                mTrack.addView(inflateAction(mimeType), index++);
+                containedTypes.remove(mimeType);
             }
         }
 
-        // Turn our list of actions into UI elements, starting with common types
-        final Set<String> containedTypes = mActions.keySet();
-        for (String mimeType : ORDERED_MIMETYPES) {
+        // Keep the current index to append non PRECEDING/FOLLOWING items.
+        final int indexAfterPreceding = index;
+
+        // Then, add FOLLOWING_MIMETYPES, which are least common.
+        for (String mimeType : FOLLOWING_MIMETYPES) {
             if (containedTypes.contains(mimeType)) {
-                final int index = mTrack.getChildCount() - 1;
-                mTrack.addView(inflateAction(mimeType), index);
+                mTrack.addView(inflateAction(mimeType), index++);
                 containedTypes.remove(mimeType);
             }
         }
 
-        // Then continue with remaining MIME-types in alphabetical order
+        // Go back to just after PRECEDING_MIMETYPES, and append the rest.
+        index = indexAfterPreceding;
         final String[] remainingTypes = containedTypes.toArray(new String[containedTypes.size()]);
         Arrays.sort(remainingTypes);
         for (String mimeType : remainingTypes) {
-            final int index = mTrack.getChildCount() - 1;
-            mTrack.addView(inflateAction(mimeType), index);
+            mTrack.addView(inflateAction(mimeType), index++);
         }
     }
 
@@ -1246,8 +1321,8 @@ public class QuickContactWindow implements Window.Callback,
     }
 
     /**
-     * Inflate the in-track view for the action of the given MIME-type. Will use
-     * the icon provided by the {@link DataKind}.
+     * Inflate the in-track view for the action of the given MIME-type, collapsing duplicate values.
+     * Will use the icon provided by the {@link DataKind}.
      */
     private View inflateAction(String mimeType) {
         final CheckableImageView view = (CheckableImageView)obtainView();
@@ -1255,6 +1330,9 @@ public class QuickContactWindow implements Window.Callback,
 
         // Add direct intent if single child, otherwise flag for multiple
         ActionList children = mActions.get(mimeType);
+        if (children.size() > 1) {
+            Collapser.collapseList(children);
+        }
         Action firstInfo = children.get(0);
         if (children.size() == 1) {
             view.setTag(firstInfo);
@@ -1330,6 +1408,7 @@ public class QuickContactWindow implements Window.Callback,
         if (tag instanceof Action) {
             // Incoming tag is concrete intent, so try launching
             final Action action = (Action)tag;
+            final boolean makePrimary = mMakePrimary;
 
             try {
                 mContext.startActivity(action.getIntent());
@@ -1339,10 +1418,10 @@ public class QuickContactWindow implements Window.Callback,
             }
 
             // Hide the resolution list, if present
-            setResolveVisible(false, actionView);
+            setResolveVisible(false, null);
             this.dismiss();
 
-            if (mMakePrimary) {
+            if (makePrimary) {
                 ContentValues values = new ContentValues(1);
                 values.put(Data.IS_SUPER_PRIMARY, 1);
                 final Uri dataUri = action.getDataUri();
@@ -1379,7 +1458,6 @@ public class QuickContactWindow implements Window.Callback,
 
                     // Set action title based on summary value
                     final Action action = (Action)getItem(position);
-                    final Drawable icon = mResolveCache.getIcon(action);
 
                     TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
                     TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
@@ -1393,7 +1471,8 @@ public class QuickContactWindow implements Window.Callback,
             });
 
             // Make sure we resize to make room for ListView
-            onWindowAttributesChanged(mWindow.getAttributes());
+            mDecor.forceLayout();
+            mDecor.invalidate();
 
         }
     }
@@ -1407,6 +1486,8 @@ public class QuickContactWindow implements Window.Callback,
         // it will close the entire dialog.
         if (mFooterDisambig.getVisibility() == View.VISIBLE) {
             setResolveVisible(false, null);
+            mDecor.forceLayout();
+            mDecor.invalidate();
         } else {
             dismiss();
         }
@@ -1463,11 +1544,11 @@ public class QuickContactWindow implements Window.Callback,
      * window, which usually means we should dismiss.
      */
     protected void detectEventOutside(MotionEvent event) {
-        if (event.getAction() == MotionEvent.ACTION_DOWN) {
+        if (event.getAction() == MotionEvent.ACTION_DOWN && mDecor != null) {
             // Only try detecting outside events on down-press
             mDecor.getHitRect(mRect);
-            mRect.top = mRect.top + mDecor.getPaddingTop();
-            mRect.bottom = mRect.bottom - mDecor.getPaddingBottom();
+            mRect.top = mRect.top + mShadowTouch;
+            mRect.bottom = mRect.bottom - mShadowTouch;
             final int x = (int)event.getX();
             final int y = (int)event.getY();
             if (!mRect.contains(x, y)) {
@@ -1542,11 +1623,6 @@ public class QuickContactWindow implements Window.Callback,
     }
 
     /** {@inheritDoc} */
-    public void onQueryEntitiesComplete(int token, Object cookie, EntityIterator iterator) {
-        // No actions
-    }
-
-    /** {@inheritDoc} */
     public void onAttachedToWindow() {
         // No actions
     }