Moved fast-track to real window, summarize multiple track icons.

Moved fast-track away from using a PopupWindow (which expected a View
to get the parent Window IBinder), and instead are using our own
Window.  This allows us to set custom parameters like dimming and
catching touches outside of our window area.

Removed FloatyListView since the fast-track no longer follows
ListView scrolling--instead outside touches dismiss the fast-track.

Changed fast-track handling of actionable contact methods to
summarize duplicate methods into one icon.  Future plan is to "fold
out" the fast-track to resolve between multiple methods.  Also
added notion of text-messaging as a separate action on a Phone item.

To prepare for duplicate resolution, we added the notion of a summary
column (from the data table) to each Mapping.  We display the resolved
value to the user when they need to pick between duplicates.  Changed
Mapping resolution to check for "common" package names first so that
phone numbers owned by other packages are all shown correctly with
generic icon.

Fixed so that pressing back button will always dismiss the fast-track
dialog when shown.  (It now behaves the same regardless of where it's
being shown.)

Added status, presence, and profile action to fast-track.  Changed
social stream so that tapping photo triggers fast-track, instead of
only using gesture approach.
diff --git a/src/com/android/contacts/ContactsActivity.java b/src/com/android/contacts/ContactsActivity.java
index 06254d3..e270c35 100644
--- a/src/com/android/contacts/ContactsActivity.java
+++ b/src/com/android/contacts/ContactsActivity.java
@@ -96,7 +96,7 @@
 
         mTabHost.addTab(mTabHost.newTabSpec("social")
                 .setIndicator(getText(R.string.socialStreamIconLabel),
-                        getResources().getDrawable(R.drawable.ic_tab_contacts))
+                        getResources().getDrawable(R.drawable.ic_tab_friends))
                 .setContent(intent));
     }
 
diff --git a/src/com/android/contacts/FastTrackWindow.java b/src/com/android/contacts/FastTrackWindow.java
index 0cf9daf..ecff1bd 100644
--- a/src/com/android/contacts/FastTrackWindow.java
+++ b/src/com/android/contacts/FastTrackWindow.java
@@ -17,9 +17,9 @@
 package com.android.contacts;
 
 import com.android.contacts.NotifyingAsyncQueryHandler.QueryCompleteListener;
-import com.android.contacts.FloatyListView.FloatyWindow;
 import com.android.contacts.SocialStreamActivity.MappingCache;
-import com.android.contacts.SocialStreamActivity.MappingCache.Mapping;
+import com.android.contacts.SocialStreamActivity.Mapping;
+import com.android.internal.policy.PolicyManager;
 import com.android.providers.contacts2.ContactsContract;
 import com.android.providers.contacts2.ContactsContract.Aggregates;
 import com.android.providers.contacts2.ContactsContract.CommonDataKinds;
@@ -30,6 +30,7 @@
 import com.android.providers.contacts2.ContactsContract.CommonDataKinds.Photo;
 import com.android.providers.contacts2.ContactsContract.CommonDataKinds.Postal;
 
+import android.app.Activity;
 import android.content.ActivityNotFoundException;
 import android.content.ContentUris;
 import android.content.Context;
@@ -39,14 +40,24 @@
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.net.Uri;
+import android.provider.Contacts.Phones;
 import android.util.Log;
+import android.view.ContextThemeWrapper;
 import android.view.Gravity;
+import android.view.KeyEvent;
 import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
 import android.view.View;
+import android.view.ViewConfiguration;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
+import android.view.Window;
+import android.view.WindowManager;
 import android.view.View.OnClickListener;
 import android.view.ViewTreeObserver.OnScrollChangedListener;
+import android.view.accessibility.AccessibilityEvent;
 import android.widget.AbsListView;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
@@ -59,135 +70,276 @@
 
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.PriorityQueue;
+import java.util.Set;
 
 /**
- * {@link PopupWindow} that shows fast-track details for a specific aggregate.
- * This window implements {@link FloatyWindow} so that it can be quickly
- * repositioned by someone like {@link FloatyListView}.
+ * Window that shows fast-track contact details for a specific
+ * {@link Aggregate#_ID}.
  */
-public class FastTrackWindow extends PopupWindow implements QueryCompleteListener, FloatyWindow {
+public class FastTrackWindow implements Window.Callback, QueryCompleteListener, OnClickListener {
     private static final String TAG = "FastTrackWindow";
 
-    private Context mContext;
-    private View mParent;
+    /**
+     * Interface used to allow the person showing a {@link FastTrackWindow} to
+     * know when the window has been dismissed.
+     */
+    interface OnDismissListener {
+        public void onDismiss(FastTrackWindow dialog);
+    }
+
+    final Context mContext;
+    final LayoutInflater mInflater;
+    final WindowManager mWindowManager;
+    Window mWindow;
+    View mDecor;
+
+    private boolean mQuerying = false;
+    private boolean mShowing = false;
 
     /** Mapping cache from mime-type to icons and actions */
     private MappingCache mMappingCache;
 
-    private ViewGroup mContent;
-
-    private Uri mDataUri;
     private NotifyingAsyncQueryHandler mHandler;
+    private OnDismissListener mDismissListener;
 
-    private boolean mShowing = false;
-    private boolean mHasPosition = false;
-    private boolean mHasDisplayName = false;
-    private boolean mHasData = false;
+    private long mAggId;
+    private int mRequestedY;
+    private int mSlop;
 
-    public static final int ICON_SIZE = 42;
-    public static final int ICON_PADDING = 3;
-    private static final int VERTICAL_OFFSET = 74;
+    private boolean mHasProfile = false;
+    private boolean mHasActions = false;
 
-    private int mFirstX;
-    private int mFirstY;
+    private ImageView mPhoto;
+    private ImageView mPresence;
+    private TextView mDisplayName;
+    private TextView mStatus;
+    private ViewGroup mTrack;
+
+    // TODO: read from a resource somewhere
+    private static final int mHeight = 150;
+    private static final int mAnchorHeight = 20;
+
+    /**
+     * Set of {@link ActionInfo} that are associated with the aggregate
+     * currently displayed by this fast-track window.
+     */
+    private ActionSet mActions = new ActionSet();
+
+    /**
+     * Specific mime-type for {@link Phone#CONTENT_ITEM_TYPE} entries that
+     * distinguishes actions that should initiate a text message.
+     */
+    public static final String MIME_SMS_ADDRESS = "vnd.android.cursor.item/sms-address";
+
+    /**
+     * Specific mime-types that should be bumped to the front of the fast-track.
+     * Other mime-types not appearing in this list follow in alphabetic order.
+     */
+    private static final String[] ORDERED_MIMETYPES = new String[] {
+        Aggregates.CONTENT_ITEM_TYPE,
+        Phones.CONTENT_ITEM_TYPE,
+        MIME_SMS_ADDRESS,
+        Email.CONTENT_ITEM_TYPE,
+    };
+
+//    public static final int ICON_SIZE = 42;
+//    public static final int ICON_PADDING = 3;
+
+    // TODO: read this status from actual query
+    private static final String STUB_STATUS = "has a really long random status message that would be far too long to read on a single device without the need for tiny reading glasses";
+
+    private static final boolean INCLUDE_PROFILE_ACTION = true;
 
     private static final int TOKEN_DISPLAY_NAME = 1;
     private static final int TOKEN_DATA = 2;
 
-    private static final int GRAVITY = Gravity.LEFT | Gravity.TOP;
-
     /** Message to show when no activity is found to perform an action */
     // TODO: move this value into a resources string
     private static final String NOT_FOUND = "Couldn't find an app to handle this action";
 
-    /** List of default mime-type icons */
-    private static HashMap<String, Integer> sMimeIcons = new HashMap<String, Integer>();
+    /**
+     * Prepare a fast-track window to show in the given {@link Context}.
+     */
+    public FastTrackWindow(Context context) {
+        mContext = new ContextThemeWrapper(context, R.style.FastTrack);
+        mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
 
-    /** List of mime-type sorting scores */
-    private static HashMap<String, Integer> sMimeScores = new HashMap<String, Integer>();
+        mSlop = ViewConfiguration.get(mContext).getScaledWindowTouchSlop();
 
-    static {
-        sMimeIcons.put(Phone.CONTENT_ITEM_TYPE, android.R.drawable.sym_action_call);
-        sMimeIcons.put(Email.CONTENT_ITEM_TYPE, android.R.drawable.sym_action_email);
-        sMimeIcons.put(Im.CONTENT_ITEM_TYPE, android.R.drawable.sym_action_chat);
-//        sMimeIcons.put(Phone.CONTENT_ITEM_TYPE, R.drawable.sym_action_sms);
-        sMimeIcons.put(Postal.CONTENT_ITEM_TYPE, R.drawable.sym_action_map);
+        mWindow = PolicyManager.makeNewWindow(mContext);
+        mWindow.setCallback(this);
+        mWindow.setWindowManager(mWindowManager, null, null);
 
-        // For scoring, put phone numbers and E-mail up front, and addresses last
-        sMimeScores.put(Phone.CONTENT_ITEM_TYPE, -200);
-        sMimeScores.put(Email.CONTENT_ITEM_TYPE, -100);
-        sMimeScores.put(Postal.CONTENT_ITEM_TYPE, 100);
+        mWindow.setContentView(R.layout.fasttrack);
+
+        mPhoto = (ImageView)mWindow.findViewById(R.id.photo);
+        mPresence = (ImageView)mWindow.findViewById(R.id.presence);
+        mDisplayName = (TextView)mWindow.findViewById(R.id.displayname);
+        mStatus = (TextView)mWindow.findViewById(R.id.status);
+        mTrack = (ViewGroup)mWindow.findViewById(R.id.fasttrack);
+
+        // TODO: move generation of mime-type cache to more-efficient place
+        generateMappingCache();
+
     }
 
     /**
-     * Create a new fast-track window for the given aggregate, using the
-     * provided {@link MappingCache} for icon as needed.
+     * Prepare a fast-track window to show in the given {@link Context}, and
+     * notify the given {@link OnDismissListener} each time this dialog is
+     * dismissed.
      */
-    public FastTrackWindow(Context context, View parent, Uri aggUri, MappingCache mappingCache) {
-        super(context);
+    public FastTrackWindow(Context context, OnDismissListener dismissListener) {
+        this(context);
+        mDismissListener = dismissListener;
+    }
 
-        final Resources resources = context.getResources();
+    /**
+     * Generate {@link MappingCache} specifically for fast-track windows. This
+     * cache knows how to display {@link CommonDataKinds#PACKAGE_COMMON} data
+     * types using generic icons.
+     */
+    private void generateMappingCache() {
+        mMappingCache = MappingCache.createAndFill(mContext);
 
-        mContext = context;
-        mParent = parent;
+        Resources res = mContext.getResources();
+        Mapping mapping;
 
-        mMappingCache = mappingCache;
+        mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, Aggregates.CONTENT_ITEM_TYPE);
+        mapping.icon = BitmapFactory.decodeResource(res, R.drawable.ic_contacts_details);
+        mMappingCache.addMapping(mapping);
 
-        // Inflate content view
-        LayoutInflater inflater = (LayoutInflater)context
-                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-        mContent = (ViewGroup)inflater.inflate(R.layout.fasttrack, null, false);
+        mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, Phone.CONTENT_ITEM_TYPE);
+        mapping.summaryColumn = Phone.NUMBER;
+        mapping.icon = BitmapFactory.decodeResource(res, android.R.drawable.sym_action_call);
+        mMappingCache.addMapping(mapping);
 
-        setContentView(mContent);
-//        setAnimationStyle(android.R.style.Animation_LeftEdge);
+        mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, MIME_SMS_ADDRESS);
+        mapping.summaryColumn = Phone.NUMBER;
+        mapping.icon = BitmapFactory.decodeResource(res, R.drawable.sym_action_sms);
+        mMappingCache.addMapping(mapping);
 
-        setBackgroundDrawable(resources.getDrawable(R.drawable.fasttrack));
+        mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, Email.CONTENT_ITEM_TYPE);
+        mapping.summaryColumn = Email.DATA;
+        mapping.icon = BitmapFactory.decodeResource(res, android.R.drawable.sym_action_email);
+        mMappingCache.addMapping(mapping);
 
-        setWidth(LayoutParams.WRAP_CONTENT);
-        setHeight(LayoutParams.WRAP_CONTENT);
+    }
 
-        setClippingEnabled(false);
-        setFocusable(false);
+    /**
+     * Start showing a fast-track window for the given {@link Aggregate#_ID}
+     * pointing towards the given location.
+     */
+    public void show(Uri aggUri, int y) {
+        if (mShowing || mQuerying) {
+            Log.w(TAG, "already in process of showing");
+            return;
+        }
+
+        mAggId = ContentUris.parseId(aggUri);
+        mRequestedY = y;
+        mQuerying = true;
 
         // Start data query in background
-        mDataUri = Uri.withAppendedPath(aggUri, ContactsContract.Aggregates.Data.CONTENT_DIRECTORY);
+        Uri dataUri = Uri.withAppendedPath(aggUri,
+                ContactsContract.Aggregates.Data.CONTENT_DIRECTORY);
 
-        mHandler = new NotifyingAsyncQueryHandler(context, this);
+        // TODO: also query for latest status message
+        mHandler = new NotifyingAsyncQueryHandler(mContext, this);
         mHandler.startQuery(TOKEN_DISPLAY_NAME, null, aggUri, null, null, null, null);
-        mHandler.startQuery(TOKEN_DATA, null, mDataUri, null, null, null, null);
+        mHandler.startQuery(TOKEN_DATA, null, dataUri, null, null, null, null);
 
-        // TODO: poll around for latest status message or location details
     }
 
     /**
-     * Consider showing this window, which requires both a given position and
-     * completed query results.
+     * Actual internal method to show this fast-track window. Called only by
+     * {@link #considerShowing()} when all data requirements have been met.
      */
-    private synchronized void considerShowing() {
-        if (mHasData && mHasPosition && mHasDisplayName && !mShowing) {
-            mShowing = true;
-            showAtLocation(mParent, GRAVITY, mFirstX, mFirstY);
+    private void showInternal() {
+        mDecor = mWindow.getDecorView();
+        WindowManager.LayoutParams l = mWindow.getAttributes();
+
+        l.gravity = Gravity.TOP | Gravity.LEFT;
+        l.x = 0;
+        l.y = mRequestedY - mHeight;
+
+        l.width = WindowManager.LayoutParams.FILL_PARENT;
+        l.height = mHeight + mAnchorHeight;
+
+        l.dimAmount = 0.6f;
+        l.flags = WindowManager.LayoutParams.FLAG_DIM_BEHIND
+                | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
+                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+                | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
+
+        mWindowManager.addView(mDecor, l);
+        mShowing = true;
+        mQuerying = false;
+    }
+
+    /**
+     * Dismiss this fast-track window if showing.
+     */
+    public void dismiss() {
+        if (!mQuerying && !mShowing) {
+            Log.d(TAG, "not visible, ignore");
+            return;
+        }
+
+        // Cancel any pending queries
+        mHandler.cancelOperation(TOKEN_DISPLAY_NAME);
+        mHandler.cancelOperation(TOKEN_DATA);
+
+        // Reset all views to prepare for possible recycling
+        mPhoto.setImageResource(R.drawable.ic_contact_list_picture);
+        mPresence.setImageDrawable(null);
+        mDisplayName.setText(null);
+        mStatus.setText(null);
+
+        mActions.clear();
+        mTrack.removeAllViews();
+
+        mHasProfile = false;
+        mHasActions = false;
+
+        if (mDecor == null || !mShowing) {
+            Log.d(TAG, "not showing, ignore");
+            return;
+        }
+
+        mWindowManager.removeView(mDecor);
+        mDecor = null;
+        mWindow.closeAllPanels();
+        mShowing = false;
+
+        // Notify any listeners that we've been dismissed
+        if (mDismissListener != null) {
+            mDismissListener.onDismiss(this);
         }
     }
 
-    /** {@inheritDoc} */
-    public void showAt(int x, int y) {
-        // Adjust vertical position by height
-        y -= VERTICAL_OFFSET;
+    /**
+     * Returns true if this fast-track window is showing or querying.
+     */
+    public boolean isShowing() {
+        return mShowing || mQuerying;
+    }
 
-        // Show dialog or update existing location
-        if (!mShowing) {
-            mFirstX = x;
-            mFirstY = y;
-            mHasPosition = true;
-            considerShowing();
-        } else {
-            update(x, y, -1, -1, true);
+    /**
+     * Consider showing this window, which will only call through to
+     * {@link #showInternal()} when all data items are present.
+     */
+    private synchronized void considerShowing() {
+        if (mHasActions && mHasProfile && !mShowing) {
+            showInternal();
         }
     }
 
@@ -195,160 +347,305 @@
     public void onQueryComplete(int token, Object cookie, Cursor cursor) {
         if (cursor == null) {
             return;
+        } else if (token == TOKEN_DISPLAY_NAME) {
+            handleDisplayName(cursor);
+        } else if (token == TOKEN_DATA) {
+            handleData(cursor);
         }
-        switch (token) {
-            case TOKEN_DISPLAY_NAME:
-                handleDisplayName(cursor);
-                break;
-            case TOKEN_DATA:
-                handleData(cursor);
-                break;
-        }
-        considerShowing();
     }
 
     /**
      * Handle the result from the {@link TOKEN_DISPLAY_NAME} query.
      */
     private void handleDisplayName(Cursor cursor) {
-        final TextView displayname = (TextView)mContent.findViewById(R.id.displayname);
         final int COL_DISPLAY_NAME = cursor.getColumnIndex(Aggregates.DISPLAY_NAME);
 
         if (cursor.moveToNext()) {
             String foundName = cursor.getString(COL_DISPLAY_NAME);
-            displayname.setText(foundName);
+            mDisplayName.setText(foundName);
+            mStatus.setText(STUB_STATUS);
         }
 
-        mHasDisplayName = true;
+        mHasProfile = true;
+        considerShowing();
+    }
+
+    /**
+     * Description of a specific, actionable {@link Data#_ID} item. May have a
+     * {@link Mapping} associated with it to find {@link RemoteViews} or icon,
+     * and may have built a summary of itself for UI display.
+     */
+    private class ActionInfo {
+        long dataId;
+        String packageName;
+        String mimeType;
+
+        Mapping mapping;
+        String summaryValue;
+
+        /**
+         * Create an action from common {@link Data} elements.
+         */
+        public ActionInfo(long dataId, String packageName, String mimeType) {
+            this.dataId = dataId;
+            this.packageName = packageName;
+            this.mimeType = mimeType;
+        }
+
+        /**
+         * Attempt to find a {@link Mapping} for the package and mime-type
+         * defined by this action. Returns true if one was found.
+         */
+        public boolean findMapping(MappingCache cache) {
+            mapping = cache.findMapping(packageName, mimeType);
+            return (mapping != null);
+        }
+
+        /**
+         * Given a {@link Cursor} pointed at the {@link Data} row associated
+         * with this action, use the {@link Mapping} to build a text summary.
+         */
+        public void buildSummary(Cursor cursor) {
+            if (mapping == null || mapping.summaryColumn == null) return;
+            int index = cursor.getColumnIndex(mapping.summaryColumn);
+            if (index != -1) {
+                summaryValue = cursor.getString(index);
+            }
+        }
+
+        /**
+         * Build an {@link Intent} that will perform this action.
+         */
+        public Intent buildIntent() {
+            // Handle well-known mime-types with special care
+            if (CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                Uri callUri = Uri.parse("tel:" + Uri.encode(summaryValue));
+                return new Intent(Intent.ACTION_DIAL, callUri);
+
+            } else if (MIME_SMS_ADDRESS.equals(mimeType)) {
+                Uri smsUri = Uri.fromParts("smsto", summaryValue, null);
+                return new Intent(Intent.ACTION_SENDTO, smsUri);
+
+            } else if (CommonDataKinds.Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                Uri mailUri = Uri.fromParts("mailto", summaryValue, null);
+                return new Intent(Intent.ACTION_SENDTO, mailUri);
+
+            }
+
+            // Otherwise fall back to default VIEW action
+            Uri dataUri = ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, dataId);
+
+            Intent intent = new Intent(Intent.ACTION_VIEW);
+            intent.setData(dataUri);
+
+            return intent;
+        }
+    }
+
+    /**
+     * Provide a simple way of collecting one or more {@link ActionInfo} objects
+     * under a mime-type key.
+     */
+    private class ActionSet extends HashMap<String, LinkedList<ActionInfo>> {
+        private void collect(String mimeType, ActionInfo info) {
+            // Create mime-type set if needed
+            if (!containsKey(mimeType)) {
+                put(mimeType, new LinkedList<ActionInfo>());
+            }
+            LinkedList<ActionInfo> collectList = get(mimeType);
+            collectList.add(info);
+        }
     }
 
     /**
      * Handle the result from the {@link TOKEN_DATA} query.
      */
     private void handleData(Cursor cursor) {
-        final ImageView photo = (ImageView)mContent.findViewById(R.id.photo);
-        final ViewGroup fastTrack = (ViewGroup)mContent.findViewById(R.id.fasttrack);
-
-        // Build list of actions for this contact, this could be done better in
-        // the future using an Adapter
-        ArrayList<ImageView> list = new ArrayList<ImageView>(cursor.getCount());
-
         final int COL_ID = cursor.getColumnIndex(Data._ID);
         final int COL_PACKAGE = cursor.getColumnIndex(Data.PACKAGE);
         final int COL_MIMETYPE = cursor.getColumnIndex(Data.MIMETYPE);
         final int COL_PHOTO = cursor.getColumnIndex(Photo.PHOTO);
 
-        boolean foundDisplayName = false;
-        boolean foundPhoto = false;
+        ActionInfo info;
+
+        // Add the profile shortcut action if requested
+        if (INCLUDE_PROFILE_ACTION) {
+            final String mimeType = Aggregates.CONTENT_ITEM_TYPE;
+            info = new ActionInfo(mAggId, CommonDataKinds.PACKAGE_COMMON, mimeType);
+            if (info.findMapping(mMappingCache)) {
+                mActions.collect(mimeType, info);
+            }
+        }
 
         while (cursor.moveToNext()) {
             final long dataId = cursor.getLong(COL_ID);
             final String packageName = cursor.getString(COL_PACKAGE);
             final String mimeType = cursor.getString(COL_MIMETYPE);
 
-            // Handle finding the photo among various return rows
-            if (!foundPhoto && Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            // Handle when a photo appears in the various data items
+            // TODO: accept a photo only if its marked as primary
+            if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
                 byte[] photoBlob = cursor.getBlob(COL_PHOTO);
                 Bitmap photoBitmap = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
-                photo.setImageBitmap(photoBitmap);
-                photo.setVisibility(View.VISIBLE);
-                foundPhoto = true;
-            }
-
-            ImageView action;
-
-            // First, try looking in mapping cache for possible icon match
-            Mapping mapping = mMappingCache.getMapping(packageName, mimeType);
-            if (mapping != null && mapping.icon != null) {
-                action = new ImageView(mContext);
-                action.setImageBitmap(mapping.icon);
-
-            } else if (sMimeIcons.containsKey(mimeType)) {
-                // Otherwise fall back to generic icons
-                int icon = sMimeIcons.get(mimeType);
-                action = new ImageView(mContext);
-                action.setImageResource(icon);
-
-            } else {
-                // No icon found, so don't insert any action button
+                mPhoto.setImageBitmap(photoBitmap);
                 continue;
-
             }
 
-            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ICON_SIZE, ICON_SIZE);
-            params.rightMargin = ICON_PADDING;
-            action.setLayoutParams(params);
-
-            // Find the sorting score for this mime-type, otherwise allocate a
-            // new one to make sure the same types are grouped together.
-            if (!sMimeScores.containsKey(mimeType)) {
-                sMimeScores.put(mimeType, sMimeScores.size());
+            // Build an action for this data entry, find a mapping to a UI
+            // element, build its summary from the cursor, and collect it along
+            // with all others of this mime-type.
+            info = new ActionInfo(dataId, packageName, mimeType);
+            if (info.findMapping(mMappingCache)) {
+                info.buildSummary(cursor);
+                mActions.collect(info.mimeType, info);
             }
 
-            int mimeScore = sMimeScores.get(mimeType);
-            action.setTag(mimeScore);
-
-            final Intent intent = buildIntentForMime(dataId, mimeType, cursor);
-            action.setOnClickListener(new OnClickListener() {
-                public void onClick(View v) {
-                    try {
-                        mContext.startActivity(intent);
-                    } catch (ActivityNotFoundException e) {
-                        Log.w(TAG, NOT_FOUND, e);
-                        Toast.makeText(mContext, NOT_FOUND, Toast.LENGTH_SHORT).show();
-                    }
+            // If phone number, also insert as text message action
+            if (Phones.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                info = new ActionInfo(dataId, packageName, MIME_SMS_ADDRESS);
+                if (info.findMapping(mMappingCache)) {
+                    info.buildSummary(cursor);
+                    mActions.collect(info.mimeType, info);
                 }
-            });
-
-            list.add(action);
+            }
         }
 
         cursor.close();
 
-        // Sort the final list based on mime-type scores
-        Collections.sort(list, new Comparator<ImageView>() {
-            public int compare(ImageView object1, ImageView object2) {
-                return (Integer)object1.getTag() - (Integer)object2.getTag();
+        // Turn our list of actions into UI elements, starting with common types
+        Set<String> containedTypes = mActions.keySet();
+        for (String mimeType : ORDERED_MIMETYPES) {
+            if (containedTypes.contains(mimeType)) {
+                mTrack.addView(inflateAction(mimeType));
+                containedTypes.remove(mimeType);
             }
-        });
-
-        for (ImageView action : list) {
-            fastTrack.addView(action);
         }
 
-        mHasData = true;
+        // Then continue with remaining mime-types in alphabetical order
+        String[] remainingTypes = containedTypes.toArray(new String[containedTypes.size()]);
+        Arrays.sort(remainingTypes);
+        for (String mimeType : remainingTypes) {
+            mTrack.addView(inflateAction(mimeType));
+        }
+
+        mHasActions = true;
+        considerShowing();
     }
 
     /**
-     * Build an {@link Intent} that will trigger the action described by the
-     * given {@link Cursor} and mime-type.
+     * Inflate the in-track view for the action of the given mime-type. Will use
+     * the icon provided by the {@link Mapping}.
      */
-    public Intent buildIntentForMime(long dataId, String mimeType, Cursor cursor) {
-        if (CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
-            final String data = cursor.getString(cursor.getColumnIndex(Phone.NUMBER));
-            Uri callUri = Uri.parse("tel:" + Uri.encode(data));
-            return new Intent(Intent.ACTION_DIAL, callUri);
+    private View inflateAction(String mimeType) {
+        ImageView view = (ImageView)mInflater.inflate(R.layout.fasttrack_item, mTrack, false);
 
-        } else if (CommonDataKinds.Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
-            final String data = cursor.getString(cursor.getColumnIndex(Email.DATA));
-            return new Intent(Intent.ACTION_SENDTO, Uri.fromParts("mailto", data, null));
-
-//        } else if (CommonDataKinds.Im.CONTENT_ITEM_TYPE.equals(mimeType)) {
-//            return new Intent(Intent.ACTION_SENDTO, constructImToUrl(host, data));
-
-        } else if (CommonDataKinds.Postal.CONTENT_ITEM_TYPE.equals(mimeType)) {
-            final String data = cursor.getString(cursor.getColumnIndex(Postal.DATA));
-            Uri mapsUri = Uri.parse("geo:0,0?q=" + Uri.encode(data));
-            return new Intent(Intent.ACTION_VIEW, mapsUri);
-
+        // Add direct intent if single child, otherwise flag for multiple
+        LinkedList<ActionInfo> children = mActions.get(mimeType);
+        ActionInfo firstInfo = children.get(0);
+        if (children.size() == 1) {
+            view.setTag(firstInfo.buildIntent());
+        } else {
+            view.setTag(mimeType);
         }
 
-        // Otherwise fall back to default VIEW action
-        Uri dataUri = ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, dataId);
+        // Set icon and listen for clicks
+        view.setImageBitmap(firstInfo.mapping.icon);
+        view.setOnClickListener(this);
+        return view;
+    }
 
-        Intent intent = new Intent(Intent.ACTION_VIEW);
-        intent.setData(dataUri);
+    /** {@inheritDoc} */
+    public void onClick(View v) {
+        final Object tag = v.getTag();
+        if (tag instanceof Intent) {
+            // Incoming tag is concrete intent, so launch
+            try {
+                mContext.startActivity((Intent)tag);
+            } catch (ActivityNotFoundException e) {
+                Log.w(TAG, NOT_FOUND);
+                Toast.makeText(mContext, NOT_FOUND, Toast.LENGTH_SHORT).show();
+            }
+        } else if (tag instanceof String) {
+            // Incoming tag is a mime-type, so show resolution list
+            LinkedList<ActionInfo> children = mActions.get(tag);
 
-        return intent;
+            // TODO: show drop-down resolution list
+            Log.d(TAG, "would show list between several options here");
+
+        }
+    }
+
+    /** {@inheritDoc} */
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        return mWindow.superDispatchKeyEvent(event);
+    }
+
+    /** {@inheritDoc} */
+    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+        // TODO: make this window accessible
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    public boolean dispatchTouchEvent(MotionEvent event) {
+        if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
+            dismiss();
+            return true;
+        }
+        return mWindow.superDispatchTouchEvent(event);
+    }
+
+    /** {@inheritDoc} */
+    public boolean dispatchTrackballEvent(MotionEvent event) {
+        return mWindow.superDispatchTrackballEvent(event);
+    }
+
+    /** {@inheritDoc} */
+    public void onContentChanged() {
+    }
+
+    /** {@inheritDoc} */
+    public boolean onCreatePanelMenu(int featureId, Menu menu) {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    public View onCreatePanelView(int featureId) {
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    public boolean onMenuItemSelected(int featureId, MenuItem item) {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    public boolean onMenuOpened(int featureId, Menu menu) {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    public void onPanelClosed(int featureId, Menu menu) {
+    }
+
+    /** {@inheritDoc} */
+    public boolean onPreparePanel(int featureId, View view, Menu menu) {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    public boolean onSearchRequested() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    public void onWindowAttributesChanged(android.view.WindowManager.LayoutParams attrs) {
+        if (mDecor != null) {
+            mWindowManager.updateViewLayout(mDecor, attrs);
+        }
+    }
+
+    /** {@inheritDoc} */
+    public void onWindowFocusChanged(boolean hasFocus) {
     }
 }
diff --git a/src/com/android/contacts/FloatyListView.java b/src/com/android/contacts/FloatyListView.java
deleted file mode 100644
index 12a25e3..0000000
--- a/src/com/android/contacts/FloatyListView.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.contacts;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.View;
-import android.widget.ListView;
-
-/**
- * Simple extension to {@link ListView} that internally keeps track of scrolling
- * position and moves any currently displayed {@link FloatyWindow}.
- */
-public class FloatyListView extends ListView {
-
-    /**
-     * Interface over to some sort of floating window that allows repositioning
-     * to a specific screen location.
-     */
-    public static interface FloatyWindow {
-        void showAt(int x, int y);
-    }
-
-    public FloatyListView(Context context) {
-        super(context);
-    }
-
-    public FloatyListView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    public FloatyListView(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
-    }
-
-    private FloatyWindow mFloaty;
-    private int mPosition;
-    private int mAnchorY;
-
-    /**
-     * Show the given {@link FloatyWindow} around the given {@link ListView}
-     * position. Specifically, it will keep the window aligned with the top of
-     * that list position.
-     */
-    public void setFloatyWindow(FloatyWindow floaty, int position) {
-        mFloaty = floaty;
-        mPosition = position;
-
-        if (mFloaty != null) {
-            mAnchorY = getPositionY();
-            mFloaty.showAt(0, mAnchorY);
-        }
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public void offsetChildrenTopAndBottom(int offset) {
-        super.offsetChildrenTopAndBottom(offset);
-
-        if (mFloaty != null) {
-            mAnchorY += offset;
-            mFloaty.showAt(0, mAnchorY);
-        }
-    }
-
-    private int[] mLocation = new int[2];
-
-    /**
-     * Calculate the current Y location of the internally tracked position.
-     */
-    public int getPositionY() {
-        return getPositionY(mPosition);
-    }
-
-    /**
-     * Calculate the current Y location of the given list position, or -1 if
-     * that position is offscreen.
-     */
-    public int getPositionY(int position) {
-        final int firstVis = getFirstVisiblePosition();
-        final int lastVis = getLastVisiblePosition();
-
-        if (position >= firstVis && position < lastVis) {
-            // Vertical position is just above position location
-            View childView = getChildAt(position - firstVis);
-            childView.getLocationOnScreen(mLocation);
-            return mLocation[1];
-        }
-        return -1;
-    }
-}
diff --git a/src/com/android/contacts/ShowOrCreateActivity.java b/src/com/android/contacts/ShowOrCreateActivity.java
index c1a3ff9..645032b 100755
--- a/src/com/android/contacts/ShowOrCreateActivity.java
+++ b/src/com/android/contacts/ShowOrCreateActivity.java
@@ -17,7 +17,6 @@
 package com.android.contacts;
 
 import com.android.contacts.NotifyingAsyncQueryHandler.QueryCompleteListener;
-import com.android.contacts.SocialStreamActivity.MappingCache;
 import com.android.providers.contacts2.ContactsContract;
 import com.android.providers.contacts2.ContactsContract.Aggregates;
 
@@ -38,11 +37,8 @@
 import android.provider.Contacts.Intents;
 import android.provider.Contacts.People;
 import android.provider.Contacts.Phones;
-import android.util.Log;
 import android.view.View;
 
-import java.lang.ref.WeakReference;
-
 /**
  * Handle several edge cases around showing or possibly creating contacts in
  * connected with a specific E-mail address or phone number. Will search based
@@ -59,7 +55,7 @@
  * {@link Intent#ACTION_SEARCH}.
  * </ul>
  */
-public final class ShowOrCreateActivity extends Activity implements QueryCompleteListener {
+public final class ShowOrCreateActivity extends Activity implements QueryCompleteListener, FastTrackWindow.OnDismissListener {
     static final String TAG = "ShowOrCreateActivity";
     static final boolean LOGD = false;
 
@@ -71,10 +67,10 @@
         ContactsContract.Contacts.AGGREGATE_ID,
 //        People._ID,
     };
-    
+
     static final String SCHEME_MAILTO = "mailto";
     static final String SCHEME_TEL = "tel";
-    
+
     static final int AGGREGATE_ID_INDEX = 0;
 
     /**
@@ -83,7 +79,7 @@
      */
     static final String QUERY_KIND_EMAIL_OR_IM = ContactMethodsColumns.KIND +
             " IN (" + Contacts.KIND_EMAIL + "," + Contacts.KIND_IM + ")";
-    
+
     /**
      * Extra used to request a specific {@link FastTrackWindow} position.
      */
@@ -91,7 +87,7 @@
     private static final int DEFAULT_Y = 90;
 
     static final int QUERY_TOKEN = 42;
-    
+
     private NotifyingAsyncQueryHandler mQueryHandler;
 
     private Bundle mCreateExtras;
@@ -99,14 +95,11 @@
     private boolean mCreateForce;
 
     private FastTrackWindow mFastTrack;
-    
+
     @Override
     protected void onCreate(Bundle icicle) {
         super.onCreate(icicle);
 
-        // Throw an empty layout up so we have a window token later
-        setContentView(R.layout.empty);
-
         // Create handler if doesn't exist, otherwise cancel any running
         if (mQueryHandler == null) {
             mQueryHandler = new NotifyingAsyncQueryHandler(this, this);
@@ -133,7 +126,7 @@
         }
 
         // Read possible extra with specific title
-        String mCreateDescrip = intent.getStringExtra(Intents.EXTRA_CREATE_DESCRIPTION);
+        mCreateDescrip = intent.getStringExtra(Intents.EXTRA_CREATE_DESCRIPTION);
         if (mCreateDescrip == null) {
             mCreateDescrip = ssp;
         }
@@ -144,11 +137,9 @@
         // Handle specific query request
         if (SCHEME_MAILTO.equals(scheme)) {
             mCreateExtras.putString(Intents.Insert.EMAIL, ssp);
-//            Uri uri = Uri.withAppendedPath(People.WITH_EMAIL_OR_IM_FILTER_URI, Uri.encode(ssp));
-//            mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
-//                    PEOPLE_PROJECTION, null, null, null);
 
-            Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_EMAIL_URI, Uri.encode(ssp));
+            Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_EMAIL_URI,
+                    Uri.encode(ssp));
             mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
                     CONTACTS_PROJECTION, null, null, null);
 
@@ -183,13 +174,14 @@
      */
     private void showFastTrack(Uri aggUri, int y) {
         // Use our local window token for now
-        final IBinder windowToken = findViewById(android.R.id.empty).getWindowToken();
-        FakeView fakeView = new FakeView(this, windowToken);
+        mFastTrack = new FastTrackWindow(this, this);
+        mFastTrack.show(aggUri, y);
+    }
 
-        final MappingCache mappingCache = MappingCache.createAndFill(this);
-
-        mFastTrack = new FastTrackWindow(this, fakeView, aggUri, mappingCache);
-        mFastTrack.showAt(0, y);
+    /** {@inheritDoc} */
+    public void onDismiss(FastTrackWindow dialog) {
+        // When dismissed, finish this activity
+        finish();
     }
 
     public void onQueryComplete(int token, Object cookie, Cursor cursor) {
diff --git a/src/com/android/contacts/SocialStreamActivity.java b/src/com/android/contacts/SocialStreamActivity.java
index 31dc652..b429a53 100644
--- a/src/com/android/contacts/SocialStreamActivity.java
+++ b/src/com/android/contacts/SocialStreamActivity.java
@@ -17,9 +17,9 @@
 package com.android.contacts;
 
 import com.android.contacts.EdgeTriggerView.EdgeTriggerListener;
-import com.android.contacts.SocialStreamActivity.MappingCache.Mapping;
 import com.android.providers.contacts2.ContactsContract;
 import com.android.providers.contacts2.ContactsContract.Aggregates;
+import com.android.providers.contacts2.ContactsContract.CommonDataKinds;
 import com.android.providers.contacts2.ContactsContract.Contacts;
 import com.android.providers.contacts2.ContactsContract.Data;
 import com.android.providers.contacts2.ContactsContract.CommonDataKinds.Photo;
@@ -36,6 +36,7 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.database.Cursor;
 import android.graphics.Bitmap;
@@ -53,9 +54,11 @@
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.Xml;
+import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.View.OnClickListener;
 import android.widget.CursorAdapter;
 import android.widget.ImageView;
 import android.widget.ListAdapter;
@@ -63,11 +66,12 @@
 import android.widget.TextView;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 
-public class SocialStreamActivity extends ListActivity implements EdgeTriggerListener {
+public class SocialStreamActivity extends ListActivity implements OnClickListener, EdgeTriggerListener {
     private static final String TAG = "SocialStreamActivity";
 
     private static final String[] PROJ_ACTIVITIES = new String[] {
@@ -101,14 +105,16 @@
     public static final int PHOTO_SIZE = 54;
     public static final int THUMBNAIL_SIZE = 54;
 
-    private ListAdapter mAdapter;
+    private SocialAdapter mAdapter;
 
-    private FloatyListView mListView;
+    private ListView mListView;
     private EdgeTriggerView mEdgeTrigger;
     private FastTrackWindow mFastTrack;
+    private MappingCache mMappingCache;
+
+    private static final boolean USE_GESTURE = false;
 
     private ContactsCache mContactsCache;
-    private MappingCache mMappingCache;
 
     @Override
     protected void onCreate(Bundle icicle) {
@@ -121,36 +127,66 @@
 
         Cursor cursor = managedQuery(Activities.CONTENT_URI, PROJ_ACTIVITIES, null, null);
         mAdapter = new SocialAdapter(this, cursor, mContactsCache, mMappingCache);
+        mAdapter.setPhotoListener(this);
 
         setListAdapter(mAdapter);
 
-        mListView = (FloatyListView)findViewById(android.R.id.list);
+        mListView = getListView();
+        mFastTrack = new FastTrackWindow(this);
 
-        // Find and listen for edge triggers
-        mEdgeTrigger = (EdgeTriggerView)findViewById(R.id.edge_trigger);
-        mEdgeTrigger.setOnEdgeTriggerListener(this);
+        if (USE_GESTURE) {
+            // Find and listen for edge triggers
+            mEdgeTrigger = (EdgeTriggerView)findViewById(R.id.edge_trigger);
+            mEdgeTrigger.setOnEdgeTriggerListener(this);
+        }
+    }
+
+    /** {@inheritDoc} */
+    public void onClick(View v) {
+        // Clicked on photo, so show fast-track
+        View listItem = (View)v.getParent();
+        showFastTrack(listItem, (Long)v.getTag());
     }
 
     /** {@inheritDoc} */
     public void onTrigger(float downX, float downY, int edge) {
         // Find list item user triggered over
-        int position = mListView.pointToPosition((int)downX, (int)downY);
+        final int position = mListView.pointToPosition((int)downX, (int)downY);
+        if (position == ListView.INVALID_POSITION) return;
+
+        // Reverse to find the exact top of the triggered entry
+        final int index = position - mListView.getFirstVisiblePosition();
+        final View anchor = mListView.getChildAt(index);
 
         Cursor cursor = (Cursor)mAdapter.getItem(position);
         long aggId = cursor.getLong(COL_AGGREGATE_ID);
 
-        Log.d(TAG, "onTrigger found position=" + position + ", contactId=" + aggId);
+        showFastTrack(anchor, aggId);
 
+    }
+
+    private int[] mLocation = new int[2];
+
+    private void showFastTrack(View anchor, long aggId) {
         Uri aggUri = ContentUris.withAppendedId(ContactsContract.Aggregates.CONTENT_URI, aggId);
 
-        // Dismiss any existing window first
-        if (mFastTrack != null) {
+        anchor.getLocationInWindow(mLocation);
+        final int entryTop = mLocation[1];
+
+        mFastTrack.dismiss();
+        mFastTrack.show(aggUri, entryTop);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        // Back key dismisses fast-track when its visible
+        if (keyCode == KeyEvent.KEYCODE_BACK && mFastTrack.isShowing()) {
             mFastTrack.dismiss();
+            return true;
         }
 
-        mFastTrack = new FastTrackWindow(this, mListView, aggUri, mMappingCache);
-        mListView.setFloatyWindow(mFastTrack, position);
-
+        return super.onKeyDown(keyCode, event);
     }
 
     @Override
@@ -177,6 +213,8 @@
         private final MappingCache mMappingCache;
         private final StyleSpan mTextStyleName;
         private final UnderlineSpan mTextStyleLink;
+        private OnClickListener mPhotoListener;
+        private SpannableStringBuilder mBuilder = new SpannableStringBuilder();
 
         private static class SocialHolder {
             ImageView photo;
@@ -200,6 +238,10 @@
             mTextStyleLink = new UnderlineSpan();
         }
 
+        public void setPhotoListener(OnClickListener listener) {
+            mPhotoListener = listener;
+        }
+
         @Override
         public int getViewTypeCount() {
             return 2;
@@ -215,6 +257,7 @@
         public void bindView(View view, Context context, Cursor cursor) {
             SocialHolder holder = (SocialHolder)view.getTag();
 
+            long aggId = cursor.getLong(COL_AGGREGATE_ID);
             long contactId = cursor.getLong(COL_AUTHOR_CONTACT_ID);
             String name = cursor.getString(COL_DISPLAY_NAME);
             String title = cursor.getString(COL_TITLE);
@@ -230,12 +273,14 @@
             } else {
                 holder.photo.setImageResource(R.drawable.ic_contact_list_picture);
             }
-            holder.contentBuilder.clear();
-            holder.contentBuilder.append(name);
-            holder.contentBuilder.append(" ");
-            holder.contentBuilder.append(title);
-            holder.contentBuilder.setSpan(mTextStyleName, 0, name.length(), 0);
-            holder.content.setText(holder.contentBuilder);
+            holder.photo.setTag(aggId);
+
+            mBuilder.clear();
+            mBuilder.append(name);
+            mBuilder.append(" ");
+            mBuilder.append(title);
+            mBuilder.setSpan(mTextStyleName, 0, name.length(), 0);
+            holder.content.setText(mBuilder);
 
             if (summary == null) {
                 holder.summary.setVisibility(View.GONE);
@@ -263,7 +308,7 @@
             if (holder.sourceIcon != null) {
                 String packageName = cursor.getString(COL_PACKAGE);
                 String mimeType = cursor.getString(COL_MIMETYPE);
-                Mapping mapping = mMappingCache.getMapping(packageName, mimeType);
+                Mapping mapping = mMappingCache.findMapping(packageName, mimeType);
                 if (mapping != null && mapping.icon != null) {
                     holder.sourceIcon.setImageBitmap(mapping.icon);
                 } else {
@@ -287,6 +332,10 @@
             holder.published = (TextView) view.findViewById(R.id.published);
             view.setTag(holder);
 
+            if (!USE_GESTURE) {
+                holder.photo.setOnClickListener(mPhotoListener);
+            }
+
             return view;
         }
 
@@ -354,10 +403,32 @@
     }
 
     /**
-     * Store a parsed <code>RemoteViewsMapping</code> object, which maps
-     * mime-types to <code>RemoteViews</code> XML resources and possible icons.
+     * Store a mapping from a package name and mime-type pair to a set of
+     * {@link RemoteViews}, default icon, and column to use from the
+     * {@link Data} table to use as a summary.
      */
-    public static class MappingCache {
+    public static class Mapping {
+        String packageName;
+        String mimeType;
+        String summaryColumn;
+        int remoteViewsRes;
+        Bitmap icon;
+
+        public Mapping() {
+        }
+
+        public Mapping(String packageName, String mimeType) {
+            this.packageName = packageName;
+            this.mimeType = mimeType;
+        }
+    }
+
+    /**
+     * Store a parsed <code>Mapping</code> object, which maps package and
+     * mime-type combinations to {@link RemoteViews} XML resources, default
+     * icons, and summary columns in the {@link Data} table.
+     */
+    public static class MappingCache extends HashMap<String, Mapping> {
         private static final String TAG = "MappingCache";
 
         private static final String TAG_MAPPINGSET = "MappingSet";
@@ -365,33 +436,45 @@
 
         private static final String MAPPING_METADATA = "com.android.contacts.stylemap";
 
-        private LinkedList<Mapping> mappings = new LinkedList<Mapping>();
 
+        /**
+         * Only allow inflating through
+         * {@link MappingCache#createAndFill(Context)}.
+         */
         private MappingCache() {
         }
 
-        public static class Mapping {
-            String packageName;
-            String mimeType;
-            int remoteViewsRes;
-            Bitmap icon;
-        }
-
+        /**
+         * Add a {@link Mapping} instance to this cache, correctly using
+         * {@link #generateKey(String, String)} when storing.
+         */
         public void addMapping(Mapping mapping) {
-            mappings.add(mapping);
+            String hashKey = generateKey(mapping.packageName, mapping.mimeType);
+            put(hashKey, mapping);
         }
 
         /**
-         * Find matching <code>RemoteViews</code> XML resource for requested
-         * package and mime-type. Returns -1 if no mapping found.
+         * Generate a key used internally for mapping a specific package name
+         * and mime-type to a {@link Mapping}.
          */
-        public Mapping getMapping(String packageName, String mimeType) {
-            for (Mapping mapping : mappings) {
-                if (mapping.packageName.equals(packageName) && mapping.mimeType.equals(mimeType)) {
-                    return mapping;
-                }
+        private String generateKey(String packageName, String mimeType) {
+            return packageName + ";" + mimeType;
+        }
+
+        /**
+         * Find matching mapping for requested package and mime-type. Returns
+         * null if no mapping found.
+         */
+        public Mapping findMapping(String packageName, String mimeType) {
+            // Search for common mapping first
+            final String commonMapping = generateKey(CommonDataKinds.PACKAGE_COMMON, mimeType);
+            if (containsKey(commonMapping)) {
+                return get(commonMapping);
             }
-            return null;
+
+            // Otherwise search for package-specific mapping
+            final String specificMapping = generateKey(packageName, mimeType);
+            return get(specificMapping);
         }
 
         /**
@@ -399,7 +482,7 @@
          * all packages to find those that provide mappings.
          */
         public static MappingCache createAndFill(Context context) {
-            Log.d(TAG, "searching for mimetype mappings...");
+            Log.d(TAG, "building mime-type mapping cache...");
             final PackageManager pm = context.getPackageManager();
             MappingCache building = new MappingCache();
             List<ApplicationInfo> installed = pm
@@ -440,6 +523,7 @@
         public void inflateMappings(Context context, int uid, String packageName,
                 XmlPullParser parser) throws InflateException {
             final AttributeSet attrs = Xml.asAttributeSet(parser);
+            final Resources res = context.getResources();
 
             try {
                 int type;
@@ -474,15 +558,13 @@
                     Mapping mapping = new Mapping();
                     mapping.packageName = packageName;
                     mapping.mimeType = a.getString(R.styleable.Mapping_mimeType);
+                    mapping.summaryColumn = a.getString(R.styleable.Mapping_summaryColumn);
                     mapping.remoteViewsRes = a.getResourceId(R.styleable.Mapping_remoteViews, -1);
 
                     // Read and resize icon if provided
                     int iconRes = a.getResourceId(R.styleable.Mapping_icon, -1);
                     if (iconRes != -1) {
-                        mapping.icon = BitmapFactory
-                                .decodeResource(context.getResources(), iconRes);
-                        mapping.icon = Utilities.createBitmapThumbnail(mapping.icon, context,
-                                FastTrackWindow.ICON_SIZE);
+                        mapping.icon = BitmapFactory.decodeResource(res, iconRes);
                     }
 
                     addMapping(mapping);