Adopted SocialContract and first pass at fast-track.

Showing live data from SocialProvider through recently added
SocialContract constants, symlinked for now since the contract
isn't in the framework yet.

Added first pass at fast-track using edge-based triggering
from the social list.  Wraps the ListView in a EdgeTriggerView
that watches for "pull" actions from a specific edge.  Also adds
concept of a FloatyListView to keep a "floaty" window anchored
with respect to ListView scrolling.

The fast-track window summarizes contact methods based on
anyone system-wide who offers an icon for the mime-types.  For
example, the testing app pushes app-specific contact methods
into the Data table, and then provides icons through its
RemoteViewsMapping XML resource.

Changed SHOW_OR_CREATE to accept Aggregate Uris and now shows
fast-track in cases where a single matching aggregate is found.

Abstracted AsyncQueryHandler to a QueryCompletedListener callback
interface to clean up code that uses it while still protecting
against leaked Contexts.
diff --git a/src/com/android/contacts/AsyncQueryHandler.java b/src/com/android/contacts/AsyncQueryHandler.java
new file mode 100644
index 0000000..fa63ad4
--- /dev/null
+++ b/src/com/android/contacts/AsyncQueryHandler.java
@@ -0,0 +1,40 @@
+package com.android.contacts;
+
+import android.content.Context;
+import android.database.Cursor;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Slightly more abstract {@link android.content.AsyncQueryHandler} that helps
+ * keep a {@link WeakReference} back to a callback interface. Will properly
+ * close the completed query if the listener ceases to exist.
+ * <p>
+ * Using this pattern will help keep you from leaking a {@link Context}.
+ */
+public class AsyncQueryHandler extends android.content.AsyncQueryHandler {
+    private final WeakReference<QueryCompleteListener> mListener;
+
+    /**
+     * Interface to listen for completed queries.
+     */
+    public static interface QueryCompleteListener {
+        public void onQueryComplete(int token, Object cookie, Cursor cursor);
+    }
+
+    public AsyncQueryHandler(Context context, QueryCompleteListener listener) {
+        super(context.getContentResolver());
+        mListener = new WeakReference<QueryCompleteListener>(listener);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+        final QueryCompleteListener listener = mListener.get();
+        if (listener != null) {
+            listener.onQueryComplete(token, cookie, cursor);
+        } else {
+            cursor.close();
+        }
+    }
+}
diff --git a/src/com/android/contacts/EdgeTriggerView.java b/src/com/android/contacts/EdgeTriggerView.java
new file mode 100644
index 0000000..d40dbad
--- /dev/null
+++ b/src/com/android/contacts/EdgeTriggerView.java
@@ -0,0 +1,140 @@
+/*
+ * 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.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+import android.widget.FrameLayout;
+
+/**
+ * Lightweight view that wraps around an existing view, watching and capturing
+ * sliding gestures from the left or right edges within a given tolerance.
+ */
+public class EdgeTriggerView extends FrameLayout {
+    public static final int FLAG_NONE = 0x00;
+    public static final int FLAG_LEFT = 0x01;
+    public static final int FLAG_RIGHT = 0x02;
+
+    private int mTouchSlop;
+
+    private int mEdgeWidth;
+    private int mListenEdges;
+
+    private boolean mListenLeft = false;
+    private boolean mListenRight = false;
+
+    private MotionEvent mDownStart;
+    private int mEdge = FLAG_NONE;
+
+    public static interface EdgeTriggerListener {
+        public void onTrigger(float downX, float downY, int edge);
+    }
+
+    private EdgeTriggerListener mListener;
+
+    /**
+     * Add a {@link EdgeTriggerListener} to watch for edge events.
+     */
+    public void setOnEdgeTriggerListener(EdgeTriggerListener listener) {
+        mListener = listener;
+    }
+
+    public EdgeTriggerView(Context context) {
+        this(context, null);
+    }
+
+    public EdgeTriggerView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public EdgeTriggerView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        setClickable(true);
+
+        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+
+        // TODO: enable reading these values again once we move away from symlinks
+//        TypedArray a = context.obtainStyledAttributes(attrs,
+//                R.styleable.EdgeTriggerView, defStyle, -1);
+//        mEdgeWidth = a.getDimensionPixelSize(R.styleable.EdgeTriggerView_edgeWidth, mTouchSlop);
+//        mListenEdges = a.getInt(R.styleable.EdgeTriggerView_listenEdges, FLAG_LEFT);
+
+        mEdgeWidth = 80;
+        mListenEdges = FLAG_LEFT;
+
+        mListenLeft = (mListenEdges & FLAG_LEFT) == FLAG_LEFT;
+        mListenRight = (mListenEdges & FLAG_RIGHT) == FLAG_RIGHT;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent event) {
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN: {
+                // Consider watching this touch event based on listening flags
+                final float x = event.getX();
+                if (mListenLeft && x < mEdgeWidth) {
+                    mEdge = FLAG_LEFT;
+                } else if (mListenRight && x > getWidth() - mEdgeWidth) {
+                    mEdge = FLAG_RIGHT;
+                } else {
+                    mEdge = FLAG_NONE;
+                }
+
+                if (mEdge != FLAG_NONE) {
+                    mDownStart = MotionEvent.obtain(event);
+                } else {
+                    mDownStart = null;
+                }
+                break;
+            }
+            case MotionEvent.ACTION_MOVE: {
+                if (mEdge != FLAG_NONE) {
+                    // If moved far enough, capture touch event for ourselves
+                    float delta = event.getX() - mDownStart.getX();
+                    if (mEdge == FLAG_LEFT && delta > mTouchSlop) {
+                        return true;
+                    } else if (mEdge == FLAG_RIGHT && delta < -mTouchSlop) {
+                        return true;
+                    }
+                }
+                break;
+            }
+        }
+
+        // Otherwise let the event slip through to children
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        // Pass trigger event to listener and return true to consume
+        if (mEdge != FLAG_NONE && mListener != null) {
+            mListener.onTrigger(mDownStart.getX(), mDownStart.getY(), mEdge);
+
+            // Reset values so we don't sent twice
+            mEdge = FLAG_NONE;
+            mDownStart = null;
+        }
+        return true;
+    }
+}
diff --git a/src/com/android/contacts/FastTrackWindow.java b/src/com/android/contacts/FastTrackWindow.java
new file mode 100644
index 0000000..b8d6ccd
--- /dev/null
+++ b/src/com/android/contacts/FastTrackWindow.java
@@ -0,0 +1,300 @@
+/*
+ * 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 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.providers.contacts2.ContactsContract;
+import com.android.providers.contacts2.ContactsContract.CommonDataKinds;
+import com.android.providers.contacts2.ContactsContract.Data;
+import com.android.providers.contacts2.ContactsContract.CommonDataKinds.Email;
+import com.android.providers.contacts2.ContactsContract.CommonDataKinds.Im;
+import com.android.providers.contacts2.ContactsContract.CommonDataKinds.Phone;
+import com.android.providers.contacts2.ContactsContract.CommonDataKinds.Postal;
+
+import android.content.ActivityNotFoundException;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.View.OnClickListener;
+import android.view.ViewTreeObserver.OnScrollChangedListener;
+import android.widget.AbsListView;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.PopupWindow;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.Gallery.LayoutParams;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.PriorityQueue;
+
+/**
+ * {@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}.
+ */
+public class FastTrackWindow extends PopupWindow implements QueryCompleteListener, FloatyWindow {
+    private static final String TAG = "FastTrackWindow";
+
+    private Context mContext;
+    private View mParent;
+
+    /** Mapping cache from mime-type to icons and actions */
+    private MappingCache mMappingCache;
+
+    private ViewGroup mContent;
+
+    private Uri mDataUri;
+    private NotifyingAsyncQueryHandler mHandler;
+
+    private boolean mShowing = false;
+    private boolean mHasPosition = false;
+    private boolean mHasData = false;
+
+    public static final int ICON_SIZE = 42;
+    public static final int ICON_PADDING = 3;
+    private static final int VERTICAL_OFFSET = 74;
+
+    private int mFirstX;
+    private int mFirstY;
+
+    private static final int TOKEN = 1;
+
+    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>();
+
+    /** List of mime-type sorting scores */
+    private static HashMap<String, Integer> sMimeScores = new HashMap<String, Integer>();
+
+    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);
+
+        // 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);
+    }
+
+    /**
+     * Create a new fast-track window for the given aggregate, using the
+     * provided {@link MappingCache} for icon as needed.
+     */
+    public FastTrackWindow(Context context, View parent, Uri aggUri, MappingCache mappingCache) {
+        super(context);
+
+        final Resources resources = context.getResources();
+
+        mContext = context;
+        mParent = parent;
+
+        mMappingCache = mappingCache;
+
+        // Inflate content view
+        LayoutInflater inflater = (LayoutInflater)context
+                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        mContent = (ViewGroup)inflater.inflate(R.layout.fasttrack, null, false);
+
+        setContentView(mContent);
+        setAnimationStyle(android.R.style.Animation_LeftEdge);
+
+        setBackgroundDrawable(resources.getDrawable(R.drawable.fasttrack));
+
+        setWidth(LayoutParams.WRAP_CONTENT);
+        setHeight(LayoutParams.WRAP_CONTENT);
+
+        setClippingEnabled(false);
+        setFocusable(false);
+
+        // Start data query in background
+        mDataUri = Uri.withAppendedPath(aggUri, ContactsContract.Aggregates.Data.CONTENT_DIRECTORY);
+
+        mHandler = new NotifyingAsyncQueryHandler(context, this);
+        mHandler.startQuery(TOKEN, null, mDataUri, null, null, null, null);
+
+    }
+
+    /**
+     * Consider showing this window, which requires both a given position and
+     * completed query results.
+     */
+    private synchronized void considerShowing() {
+        if (mHasData && mHasPosition && !mShowing) {
+            mShowing = true;
+            showAtLocation(mParent, GRAVITY, mFirstX, mFirstY);
+        }
+    }
+
+    /** {@inheritDoc} */
+    public void showAt(int x, int y) {
+        // Adjust vertical position by height
+        y -= VERTICAL_OFFSET;
+
+        // Show dialog or update existing location
+        if (!mShowing) {
+            mFirstX = x;
+            mFirstY = y;
+            mHasPosition = true;
+            considerShowing();
+        } else {
+            update(x, y, -1, -1, true);
+        }
+    }
+
+    /** {@inheritDoc} */
+    public void onQueryComplete(int token, Object cookie, Cursor cursor) {
+        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);
+
+        while (cursor.moveToNext()) {
+            final long dataId = cursor.getLong(COL_ID);
+            final String packageName = cursor.getString(COL_PACKAGE);
+            final String mimeType = cursor.getString(COL_MIMETYPE);
+
+            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
+                break;
+
+            }
+
+            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());
+            }
+
+            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();
+                    }
+                }
+            });
+
+            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();
+            }
+        });
+
+        for (ImageView action : list) {
+            fastTrack.addView(action);
+        }
+
+        mHasData = true;
+        considerShowing();
+    }
+
+    /**
+     * Build an {@link Intent} that will trigger the action described by the
+     * given {@link Cursor} and mime-type.
+     */
+    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);
+
+        } 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);
+
+        }
+
+        // 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;
+    }
+}
diff --git a/src/com/android/contacts/FloatyListView.java b/src/com/android/contacts/FloatyListView.java
new file mode 100644
index 0000000..12a25e3
--- /dev/null
+++ b/src/com/android/contacts/FloatyListView.java
@@ -0,0 +1,107 @@
+/*
+ * 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/NotifyingAsyncQueryHandler.java b/src/com/android/contacts/NotifyingAsyncQueryHandler.java
new file mode 100644
index 0000000..2223e9c
--- /dev/null
+++ b/src/com/android/contacts/NotifyingAsyncQueryHandler.java
@@ -0,0 +1,41 @@
+package com.android.contacts;
+
+import android.content.AsyncQueryHandler;
+import android.content.Context;
+import android.database.Cursor;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Slightly more abstract {@link android.content.AsyncQueryHandler} that helps
+ * keep a {@link WeakReference} back to a callback interface. Will properly
+ * close the completed query if the listener ceases to exist.
+ * <p>
+ * Using this pattern will help keep you from leaking a {@link Context}.
+ */
+public class NotifyingAsyncQueryHandler extends AsyncQueryHandler {
+    private final WeakReference<QueryCompleteListener> mListener;
+
+    /**
+     * Interface to listen for completed queries.
+     */
+    public static interface QueryCompleteListener {
+        public void onQueryComplete(int token, Object cookie, Cursor cursor);
+    }
+
+    public NotifyingAsyncQueryHandler(Context context, QueryCompleteListener listener) {
+        super(context.getContentResolver());
+        mListener = new WeakReference<QueryCompleteListener>(listener);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+        final QueryCompleteListener listener = mListener.get();
+        if (listener != null) {
+            listener.onQueryComplete(token, cookie, cursor);
+        } else {
+            cursor.close();
+        }
+    }
+}
diff --git a/src/com/android/contacts/ShowOrCreateActivity.java b/src/com/android/contacts/ShowOrCreateActivity.java
index 75af4ae..56aef26 100755
--- a/src/com/android/contacts/ShowOrCreateActivity.java
+++ b/src/com/android/contacts/ShowOrCreateActivity.java
@@ -16,16 +16,22 @@
 
 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;
+
 import android.app.Activity;
 import android.app.AlertDialog;
-import android.content.AsyncQueryHandler;
 import android.content.ComponentName;
 import android.content.ContentUris;
+import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.IBinder;
 import android.provider.Contacts;
 import android.provider.Contacts.ContactMethods;
 import android.provider.Contacts.ContactMethodsColumns;
@@ -33,6 +39,7 @@
 import android.provider.Contacts.People;
 import android.provider.Contacts.Phones;
 import android.util.Log;
+import android.view.View;
 
 import java.lang.ref.WeakReference;
 
@@ -52,7 +59,7 @@
  * {@link Intent#ACTION_SEARCH}.
  * </ul>
  */
-public final class ShowOrCreateActivity extends Activity {
+public final class ShowOrCreateActivity extends Activity implements QueryCompleteListener {
     static final String TAG = "ShowOrCreateActivity";
     static final boolean LOGD = false;
 
@@ -60,14 +67,15 @@
         Phones.PERSON_ID,
     };
 
-    static final String[] PEOPLE_PROJECTION = new String[] {
-        People._ID,
+    static final String[] CONTACTS_PROJECTION = new String[] {
+        ContactsContract.Contacts.AGGREGATE_ID,
+//        People._ID,
     };
     
     static final String SCHEME_MAILTO = "mailto";
     static final String SCHEME_TEL = "tel";
     
-    static final int PERSON_ID_INDEX = 0;
+    static final int AGGREGATE_ID_INDEX = 0;
 
     /**
      * Query clause to filter {@link ContactMethods#CONTENT_URI} to only search
@@ -76,24 +84,39 @@
     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.
+     */
+    private static final String EXTRA_Y = "pixel_y";
+    private static final int DEFAULT_Y = 90;
+
     static final int QUERY_TOKEN = 42;
     
-    private QueryHandler mQueryHandler;
+    private NotifyingAsyncQueryHandler mQueryHandler;
+
+    private Bundle mCreateExtras;
+    private String mCreateDescrip;
+    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 QueryHandler(this);
+            mQueryHandler = new NotifyingAsyncQueryHandler(this, this);
         } else {
             mQueryHandler.cancelOperation(QUERY_TOKEN);
         }
 
         final Intent intent = getIntent();
         final Uri data = intent.getData();
-        
+
         // Unpack scheme and target data from intent
         String scheme = null;
         String ssp = null;
@@ -101,52 +124,143 @@
             scheme = data.getScheme();
             ssp = data.getSchemeSpecificPart();
         }
-        
+
         // Build set of extras for possible use when creating contact
-        Bundle createExtras = new Bundle();
+        mCreateExtras = new Bundle();
         Bundle originalExtras = intent.getExtras();
         if (originalExtras != null) {
-            createExtras.putAll(originalExtras);
+            mCreateExtras.putAll(originalExtras);
         }
-        mQueryHandler.setCreateExtras(createExtras);
-        
+
         // Read possible extra with specific title
-        String createDescrip = intent.getStringExtra(Intents.EXTRA_CREATE_DESCRIPTION);
-        if (createDescrip == null) {
-            createDescrip = ssp;
+        String mCreateDescrip = intent.getStringExtra(Intents.EXTRA_CREATE_DESCRIPTION);
+        if (mCreateDescrip == null) {
+            mCreateDescrip = ssp;
         }
-        mQueryHandler.setCreateDescription(createDescrip);
-        
+
         // Allow caller to bypass dialog prompt
-        boolean createForce = intent.getBooleanExtra(Intents.EXTRA_FORCE_CREATE, false);
-        mQueryHandler.setCreateForce(createForce);
-        
+        mCreateForce = intent.getBooleanExtra(Intents.EXTRA_FORCE_CREATE, false);
+
         // Handle specific query request
         if (SCHEME_MAILTO.equals(scheme)) {
-            createExtras.putString(Intents.Insert.EMAIL, ssp);
-            Uri uri = Uri.withAppendedPath(People.WITH_EMAIL_OR_IM_FILTER_URI, Uri.encode(ssp));
+            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_EMAIL_FILTER_URI, Uri.encode(ssp));
             mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
-                    PEOPLE_PROJECTION, null, null, null);
+                    CONTACTS_PROJECTION, null, null, null);
+
         } else if (SCHEME_TEL.equals(scheme)) {
-            createExtras.putString(Intents.Insert.PHONE, ssp);
-            mQueryHandler.startQuery(QUERY_TOKEN, null,
-                    Uri.withAppendedPath(Phones.CONTENT_FILTER_URL, ssp),
-                    PHONES_PROJECTION, null, null, null);
-            
+            mCreateExtras.putString(Intents.Insert.PHONE, ssp);
+//            mQueryHandler.startQuery(QUERY_TOKEN, null,
+//                    Uri.withAppendedPath(Phones.CONTENT_FILTER_URL, ssp),
+//                    PHONES_PROJECTION, null, null, null);
+
         } else {
-            Log.w(TAG, "Invalid intent:" + getIntent());
-            finish();
+            // Otherwise assume incoming aggregate Uri
+            final int y = getIntent().getExtras().getInt(EXTRA_Y, DEFAULT_Y);
+            showFastTrack(data, y);
+
         }
     }
-    
+
     @Override
     protected void onStop() {
         super.onStop();
         if (mQueryHandler != null) {
             mQueryHandler.cancelOperation(QUERY_TOKEN);
         }
+        if (mFastTrack != null) {
+            mFastTrack.dismiss();
+        }
     }
-    
+
+    /**
+     * Show a {@link FastTrackWindow} for the given aggregate at the requested
+     * screen location.
+     */
+    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);
+
+        final MappingCache mappingCache = MappingCache.createAndFill(this);
+
+        mFastTrack = new FastTrackWindow(this, fakeView, aggUri, mappingCache);
+        mFastTrack.showAt(0, y);
+    }
+
+    public void onQueryComplete(int token, Object cookie, Cursor cursor) {
+        if (cursor == null) {
+            return;
+        }
+
+        // Count contacts found by query
+        int count = 0;
+        long aggId = -1;
+        try {
+            count = cursor.getCount();
+            if (count == 1 && cursor.moveToFirst()) {
+                // Try reading ID if only one contact returned
+                aggId = cursor.getLong(AGGREGATE_ID_INDEX);
+            }
+        } finally {
+            cursor.close();
+        }
+
+        if (count == 1 && aggId != -1) {
+            // If we only found one item, jump right to viewing it
+            final Uri aggUri = ContentUris.withAppendedId(Aggregates.CONTENT_URI, aggId);
+            final int y = getIntent().getExtras().getInt(EXTRA_Y, DEFAULT_Y);
+            showFastTrack(aggUri, y);
+
+//            Intent viewIntent = new Intent(Intent.ACTION_VIEW,
+//                    ContentUris.withAppendedId(People.CONTENT_URI, personId));
+//            activity.startActivity(viewIntent);
+//            activity.finish();
+
+        } else if (count > 1) {
+            // If more than one, show pick list
+            Intent listIntent = new Intent(Intent.ACTION_SEARCH);
+            listIntent.setComponent(new ComponentName(this, ContactsListActivity.class));
+            listIntent.putExtras(mCreateExtras);
+            startActivity(listIntent);
+            finish();
+
+        } else {
+            // No matching contacts found
+            if (mCreateForce) {
+                // Forced to create new contact
+                Intent createIntent = new Intent(Intent.ACTION_INSERT, People.CONTENT_URI);
+                createIntent.putExtras(mCreateExtras);
+                createIntent.setType(People.CONTENT_TYPE);
+
+                startActivity(createIntent);
+                finish();
+
+            } else {
+                // Prompt user to insert or edit contact
+                Intent createIntent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
+                createIntent.putExtras(mCreateExtras);
+                createIntent.setType(People.CONTENT_ITEM_TYPE);
+
+                CharSequence message = getResources().getString(
+                        R.string.add_contact_dlg_message_fmt, mCreateDescrip);
+
+                new AlertDialog.Builder(this)
+                        .setTitle(R.string.add_contact_dlg_title)
+                        .setMessage(message)
+                        .setPositiveButton(android.R.string.ok,
+                                new IntentClickListener(this, createIntent))
+                        .setNegativeButton(android.R.string.cancel,
+                                new IntentClickListener(this, null))
+                        .show();
+            }
+        }
+    }
+
     /**
      * Listener for {@link DialogInterface} that launches a given {@link Intent}
      * when clicked. When clicked, this also closes the parent using
@@ -174,101 +288,20 @@
     }
 
     /**
-     * Handle asynchronous query to find matching contacts. When query finishes,
-     * will handle based on number of matching contacts found.
+     * Fake view that simply exists to pass through a specific {@link IBinder}
+     * window token.
      */
-    private static final class QueryHandler extends AsyncQueryHandler {
-        private final WeakReference<Activity> mActivity;
-        private Bundle mCreateExtras;
-        private String mCreateDescrip;
-        private boolean mCreateForce;
+    private static class FakeView extends View {
+        private IBinder mWindowToken;
 
-        public QueryHandler(Activity activity) {
-            super(activity.getContentResolver());
-            mActivity = new WeakReference<Activity>(activity);
-        }
-        
-        public void setCreateExtras(Bundle createExtras) {
-            mCreateExtras = createExtras;
-        }
-        
-        public void setCreateDescription(String createDescrip) {
-            mCreateDescrip = createDescrip;
-        }
-        
-        public void setCreateForce(boolean createForce) {
-            mCreateForce = createForce;
+        public FakeView(Context context, IBinder windowToken) {
+            super(context);
+            mWindowToken = windowToken;
         }
 
         @Override
-        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
-            Activity activity = mActivity.get();
-            if (activity == null) {
-                return;
-            }
-            
-            // Count contacts found by query
-            int count = 0;
-            long personId = -1;
-            if (cursor != null) {
-                try {
-                    count = cursor.getCount();
-                    if (count == 1 && cursor.moveToFirst()) {
-                        // Try reading ID if only one contact returned
-                        personId = cursor.getLong(PERSON_ID_INDEX);
-                    }
-                } finally {
-                    cursor.close();
-                }
-            }
-            
-            if (LOGD) Log.d(TAG, "onQueryComplete count=" + count);
-            
-            if (count == 1) {
-                // If we only found one item, jump right to viewing it
-                Intent viewIntent = new Intent(Intent.ACTION_VIEW,
-                        ContentUris.withAppendedId(People.CONTENT_URI, personId));
-                activity.startActivity(viewIntent);
-                activity.finish();
-                
-            } else if (count > 1) {
-                // If more than one, show pick list
-                Intent listIntent = new Intent(Intent.ACTION_SEARCH);
-                listIntent.setComponent(new ComponentName(activity, ContactsListActivity.class));
-                listIntent.putExtras(mCreateExtras);
-                activity.startActivity(listIntent);
-                activity.finish();
-                
-            } else {
-                // No matching contacts found
-                if (mCreateForce) {
-                    // Forced to create new contact
-                    Intent createIntent = new Intent(Intent.ACTION_INSERT, People.CONTENT_URI);
-                    createIntent.putExtras(mCreateExtras);
-                    createIntent.setType(People.CONTENT_TYPE);
-                    
-                    activity.startActivity(createIntent);
-                    activity.finish();
-                    
-                } else {
-                    // Prompt user to insert or edit contact 
-                    Intent createIntent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
-                    createIntent.putExtras(mCreateExtras);
-                    createIntent.setType(People.CONTENT_ITEM_TYPE);
-                    
-                    CharSequence message = activity.getResources().getString(
-                            R.string.add_contact_dlg_message_fmt, mCreateDescrip);
-                    
-                    new AlertDialog.Builder(activity)
-                            .setTitle(R.string.add_contact_dlg_title)
-                            .setMessage(message)
-                            .setPositiveButton(android.R.string.ok,
-                                    new IntentClickListener(activity, createIntent))
-                            .setNegativeButton(android.R.string.cancel,
-                                    new IntentClickListener(activity, null))
-                            .show();
-                }
-            }
+        public IBinder getWindowToken() {
+            return mWindowToken;
         }
     }
 }
diff --git a/src/com/android/contacts/SocialStreamActivity.java b/src/com/android/contacts/SocialStreamActivity.java
index 8917fb4..e04128f 100644
--- a/src/com/android/contacts/SocialStreamActivity.java
+++ b/src/com/android/contacts/SocialStreamActivity.java
@@ -16,19 +16,447 @@
 
 package com.android.contacts;
 
-import android.app.ListActivity;
-import android.os.Bundle;
-import android.widget.ArrayAdapter;
+import com.android.contacts.EdgeTriggerView.EdgeTriggerListener;
+import com.android.providers.contacts2.ContactsContract;
+import com.android.providers.contacts2.ContactsContract.Aggregates;
+import com.android.providers.contacts2.ContactsContract.Contacts;
+import com.android.providers.contacts2.ContactsContract.Data;
+import com.android.providers.contacts2.ContactsContract.CommonDataKinds.Photo;
+import com.android.providers.contacts2.ContactsContract.CommonDataKinds.StructuredName;
+import com.android.providers.contacts2.SocialContract.Activities;
 
-public class SocialStreamActivity extends ListActivity {
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.app.ListActivity;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+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;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PaintFlagsDrawFilter;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.PaintDrawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.format.DateUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Xml;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.CursorAdapter;
+import android.widget.ImageView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.SimpleCursorAdapter;
+import android.widget.TextView;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+public class SocialStreamActivity extends ListActivity implements EdgeTriggerListener {
+    private static final String TAG = "SocialStreamActivity";
+
+    private static final String[] PROJ_ACTIVITIES = new String[] {
+        Activities._ID,
+        Activities.PACKAGE,
+        Activities.MIMETYPE,
+        Activities.AUTHOR_CONTACT_ID,
+        Contacts.AGGREGATE_ID,
+        Aggregates.DISPLAY_NAME,
+        Activities.PUBLISHED,
+        Activities.TITLE,
+        Activities.SUMMARY,
+    };
+
+    private static final int COL_ID = 0;
+    private static final int COL_PACKAGE = 1;
+    private static final int COL_MIMETYPE = 2;
+    private static final int COL_AUTHOR_CONTACT_ID = 3;
+    private static final int COL_AGGREGATE_ID = 4;
+    private static final int COL_DISPLAY_NAME = 5;
+    private static final int COL_PUBLISHED = 6;
+    private static final int COL_TITLE = 7;
+    private static final int COL_SUMMARY = 8;
+
+    public static final int PHOTO_SIZE = 58;
+
+    private ListAdapter mAdapter;
+
+    private FloatyListView mListView;
+    private EdgeTriggerView mEdgeTrigger;
+    private FastTrackWindow mFastTrack;
+
+    private ContactsCache mContactsCache;
+    private MappingCache mMappingCache;
 
     @Override
     protected void onCreate(Bundle icicle) {
         super.onCreate(icicle);
 
-        setContentView(R.layout.social_list_content);
+        setContentView(R.layout.social_list);
 
-        setListAdapter(new ArrayAdapter<String>(this,
-                android.R.layout.simple_list_item_1));
+        mContactsCache = new ContactsCache(this);
+        mMappingCache = MappingCache.createAndFill(this);
+
+        Cursor cursor = managedQuery(Activities.CONTENT_URI, PROJ_ACTIVITIES, null, null);
+        mAdapter = new SocialAdapter(this, cursor, mContactsCache);
+
+//        mAdapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_2, cursor,
+//                new String[] { Activities.AUTHOR_CONTACT_ID, Activities.TITLE },
+//                new int[] { android.R.id.text1, android.R.id.text2 });
+
+        setListAdapter(mAdapter);
+
+        mListView = (FloatyListView)findViewById(android.R.id.list);
+
+        // Find and listen for edge triggers
+        mEdgeTrigger = (EdgeTriggerView)findViewById(R.id.edge_trigger);
+        mEdgeTrigger.setOnEdgeTriggerListener(this);
+    }
+
+    /** {@inheritDoc} */
+    public void onTrigger(float downX, float downY, int edge) {
+        // Find list item user triggered over
+        int position = mListView.pointToPosition((int)downX, (int)downY);
+
+        Cursor cursor = (Cursor)mAdapter.getItem(position);
+        long aggId = cursor.getLong(COL_AGGREGATE_ID);
+
+        Log.d(TAG, "onTrigger found position=" + position + ", contactId=" + aggId);
+
+        Uri aggUri = ContentUris.withAppendedId(ContactsContract.Aggregates.CONTENT_URI, aggId);
+
+        // Dismiss any existing window first
+        if (mFastTrack != null) {
+            mFastTrack.dismiss();
+        }
+
+        mFastTrack = new FastTrackWindow(this, mListView, aggUri, mMappingCache);
+        mListView.setFloatyWindow(mFastTrack, position);
+
+    }
+
+    /**
+     * List adapter for social stream data queried from
+     * {@link Activities#CONTENT_URI}.
+     */
+    private static class SocialAdapter extends CursorAdapter {
+        private Context mContext;
+        private LayoutInflater mInflater;
+        private ContactsCache mContactsCache;
+
+        private static class SocialHolder {
+            ImageView photo;
+            TextView displayname;
+            TextView title;
+            TextView published;
+        }
+
+        public SocialAdapter(Context context, Cursor c, ContactsCache contactsCache) {
+            super(context, c, true);
+            mContext = context;
+            mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+            mContactsCache = contactsCache;
+        }
+
+        @Override
+        public void bindView(View view, Context context, Cursor cursor) {
+            SocialHolder holder = (SocialHolder)view.getTag();
+
+            long contactId = cursor.getLong(COL_AUTHOR_CONTACT_ID);
+
+            // TODO: trigger async query to find actual name and photo instead
+            // of using this lazy caching mechanism
+            holder.photo.setImageBitmap(mContactsCache.getPhoto(contactId));
+
+            holder.displayname.setText(cursor.getString(COL_DISPLAY_NAME));
+            holder.title.setText(cursor.getString(COL_TITLE));
+
+            long published = cursor.getLong(COL_PUBLISHED);
+            CharSequence relativePublished = DateUtils.getRelativeTimeSpanString(published, System
+                    .currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS);
+            holder.published.setText(relativePublished);
+
+        }
+
+        @Override
+        public View newView(Context context, Cursor cursor, ViewGroup parent) {
+            View view = mInflater.inflate(R.layout.social_list_item, parent, false);
+
+            SocialHolder holder = new SocialHolder();
+            holder.photo = (ImageView)view.findViewById(R.id.photo);
+            holder.displayname = (TextView)view.findViewById(R.id.displayname);
+            holder.title = (TextView)view.findViewById(R.id.title);
+            holder.published = (TextView)view.findViewById(R.id.published);
+            view.setTag(holder);
+
+            return view;
+        }
+    }
+
+    /**
+     * Keep a cache that maps from {@link Contacts#_ID} to {@link Photo#PHOTO}
+     * values.
+     */
+    private static class ContactsCache {
+        private static final String TAG = "ContactsCache";
+
+        private static final String[] PROJ_DETAILS = new String[] {
+            Data.MIMETYPE,
+            Data.CONTACT_ID,
+            Photo.PHOTO,
+        };
+
+        private static final int COL_MIMETYPE = 0;
+        private static final int COL_CONTACT_ID = 1;
+        private static final int COL_PHOTO = 3;
+
+        private HashMap<Long, Bitmap> mPhoto = new HashMap<Long, Bitmap>();
+
+        public ContactsCache(Context context) {
+            Log.d(TAG, "building ContactsCache...");
+
+            ContentResolver resolver = context.getContentResolver();
+            Cursor cursor = resolver.query(Data.CONTENT_URI, PROJ_DETAILS,
+                    Data.MIMETYPE + "=?", new String[] { Photo.CONTENT_ITEM_TYPE }, null);
+
+            while (cursor.moveToNext()) {
+                long contactId = cursor.getLong(COL_CONTACT_ID);
+                String mimeType = cursor.getString(COL_MIMETYPE);
+                if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                    byte[] photoBlob = cursor.getBlob(COL_PHOTO);
+                    Bitmap photo = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
+                    photo = Utilities.createBitmapThumbnail(photo, context, PHOTO_SIZE);
+
+                    mPhoto.put(contactId, photo);
+                }
+            }
+
+            cursor.close();
+            Log.d(TAG, "done building ContactsCache");
+        }
+
+        public Bitmap getPhoto(long contactId) {
+            return mPhoto.get(contactId);
+        }
+    }
+
+    /**
+     * Store a parsed <code>RemoteViewsMapping</code> object, which maps
+     * mime-types to <code>RemoteViews</code> XML resources and possible icons.
+     */
+    public static class MappingCache {
+        private static final String TAG = "MappingCache";
+
+        private static final String TAG_MAPPINGSET = "MappingSet";
+        private static final String TAG_MAPPING = "Mapping";
+
+        private static final String MAPPING_METADATA = "com.android.contacts.stylemap";
+
+        private LinkedList<Mapping> mappings = new LinkedList<Mapping>();
+
+        private MappingCache() {
+        }
+
+        public static class Mapping {
+            String packageName;
+            String mimeType;
+            int remoteViewsRes;
+            Bitmap icon;
+        }
+
+        public void addMapping(Mapping mapping) {
+            mappings.add(mapping);
+        }
+
+        /**
+         * Find matching <code>RemoteViews</code> XML resource for requested
+         * package and mime-type. Returns -1 if no mapping found.
+         */
+        public Mapping getMapping(String packageName, String mimeType) {
+            for (Mapping mapping : mappings) {
+                if (mapping.packageName.equals(packageName) && mapping.mimeType.equals(mimeType)) {
+                    return mapping;
+                }
+            }
+            return null;
+        }
+
+        /**
+         * Create a new {@link MappingCache} object and fill by walking across
+         * all packages to find those that provide mappings.
+         */
+        public static MappingCache createAndFill(Context context) {
+            Log.d(TAG, "searching for mimetype mappings...");
+            final PackageManager pm = context.getPackageManager();
+            MappingCache building = new MappingCache();
+            List<ApplicationInfo> installed = pm
+                    .getInstalledApplications(PackageManager.GET_META_DATA);
+            for (ApplicationInfo info : installed) {
+                if (info.metaData != null && info.metaData.containsKey(MAPPING_METADATA)) {
+                    try {
+                        // Found metadata, so clone into their context to
+                        // inflate reference
+                        Context theirContext = context.createPackageContext(info.packageName, 0);
+                        XmlPullParser mappingParser = info.loadXmlMetaData(pm, MAPPING_METADATA);
+                        building.inflateMappings(theirContext, info.uid, info.packageName,
+                                mappingParser);
+                    } catch (NameNotFoundException e) {
+                        Log.w(TAG, "Problem creating context for remote package", e);
+                    } catch (InflateException e) {
+                        Log.w(TAG, "Problem inflating MappingSet from remote package", e);
+                    }
+                }
+            }
+            return building;
+        }
+
+        public static class InflateException extends Exception {
+            public InflateException(String message) {
+                super(message);
+            }
+
+            public InflateException(String message, Throwable throwable) {
+                super(message, throwable);
+            }
+        }
+
+        /**
+         * Inflate a <code>MappingSet</code> from an XML resource, assuming the
+         * given package name as the source.
+         */
+        public void inflateMappings(Context context, int uid, String packageName,
+                XmlPullParser parser) throws InflateException {
+            final AttributeSet attrs = Xml.asAttributeSet(parser);
+
+            try {
+                int type;
+                while ((type = parser.next()) != XmlPullParser.START_TAG
+                        && type != XmlPullParser.END_DOCUMENT) {
+                    // Drain comments and whitespace
+                }
+
+                if (type != XmlPullParser.START_TAG) {
+                    throw new InflateException("No start tag found");
+                }
+
+                if (!TAG_MAPPINGSET.equals(parser.getName())) {
+                    throw new InflateException("Top level element must be MappingSet");
+                }
+
+                // Parse all children actions
+                final int depth = parser.getDepth();
+                while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
+                        && type != XmlPullParser.END_DOCUMENT) {
+                    if (type == XmlPullParser.END_TAG)
+                        continue;
+
+                    if (!TAG_MAPPING.equals(parser.getName())) {
+                        throw new InflateException("Expected Mapping tag");
+                    }
+
+                    // Parse kind, mime-type, and RemoteViews reference
+                    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Mapping);
+
+                    Mapping mapping = new Mapping();
+                    mapping.packageName = packageName;
+                    mapping.mimeType = a.getString(R.styleable.Mapping_mimeType);
+                    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);
+                    }
+
+                    addMapping(mapping);
+                    Log.d(TAG, "Added mapping for packageName=" + mapping.packageName
+                            + ", mimetype=" + mapping.mimeType);
+                }
+            } catch (XmlPullParserException e) {
+                throw new InflateException("Problem reading XML", e);
+            } catch (IOException e) {
+                throw new InflateException("Problem reading XML", e);
+            }
+        }
+    }
+
+    /**
+     * Borrowed from Launcher for {@link Bitmap} resizing.
+     */
+    static final class Utilities {
+        private static final Paint sPaint = new Paint();
+        private static final Rect sBounds = new Rect();
+        private static final Rect sOldBounds = new Rect();
+        private static Canvas sCanvas = new Canvas();
+
+        static {
+            sCanvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.DITHER_FLAG,
+                    Paint.FILTER_BITMAP_FLAG));
+        }
+
+        /**
+         * Returns a Bitmap representing the thumbnail of the specified Bitmap.
+         * The size of the thumbnail is defined by the dimension
+         * android.R.dimen.launcher_application_icon_size. This method is not
+         * thread-safe and should be invoked on the UI thread only.
+         * 
+         * @param bitmap The bitmap to get a thumbnail of.
+         * @param context The application's context.
+         * @return A thumbnail for the specified bitmap or the bitmap itself if
+         *         the thumbnail could not be created.
+         */
+        static Bitmap createBitmapThumbnail(Bitmap bitmap, Context context, int size) {
+            int width = size;
+            int height = size;
+
+            final int bitmapWidth = bitmap.getWidth();
+            final int bitmapHeight = bitmap.getHeight();
+
+            if (width > 0 && height > 0 && (width < bitmapWidth || height < bitmapHeight)) {
+                final float ratio = (float)bitmapWidth / bitmapHeight;
+
+                if (bitmapWidth > bitmapHeight) {
+                    height = (int)(width / ratio);
+                } else if (bitmapHeight > bitmapWidth) {
+                    width = (int)(height * ratio);
+                }
+
+                final Bitmap.Config c = (width == size && height == size) ? bitmap.getConfig()
+                        : Bitmap.Config.ARGB_8888;
+                final Bitmap thumb = Bitmap.createBitmap(size, size, c);
+                final Canvas canvas = sCanvas;
+                final Paint paint = sPaint;
+                canvas.setBitmap(thumb);
+                paint.setDither(false);
+                paint.setFilterBitmap(true);
+                sBounds.set((size - width) / 2, (size - height) / 2, width, height);
+                sOldBounds.set(0, 0, bitmapWidth, bitmapHeight);
+                canvas.drawBitmap(bitmap, sOldBounds, sBounds, paint);
+                return thumb;
+            }
+
+            return bitmap;
+        }
     }
 }
diff --git a/src/com/android/providers/contacts2/ContactsContract.java b/src/com/android/providers/contacts2/ContactsContract.java
new file mode 120000
index 0000000..206b35a
--- /dev/null
+++ b/src/com/android/providers/contacts2/ContactsContract.java
@@ -0,0 +1 @@
+../../../../../../../providers/ContactsProvider2/src/com/android/providers/contacts2/ContactsContract.java
\ No newline at end of file
diff --git a/src/com/android/providers/contacts2/SocialContract.java b/src/com/android/providers/contacts2/SocialContract.java
new file mode 120000
index 0000000..40cafe1
--- /dev/null
+++ b/src/com/android/providers/contacts2/SocialContract.java
@@ -0,0 +1 @@
+../../../../../../../providers/ContactsProvider2/src/com/android/providers/contacts2/SocialContract.java
\ No newline at end of file