OSDN Git Service

Single query with social, IM rows, heavy recycling, large.
authorJeff Sharkey <jsharkey@android.com>
Wed, 30 Sep 2009 21:57:07 +0000 (14:57 -0700)
committerJeff Sharkey <jsharkey@android.com>
Thu, 1 Oct 2009 11:56:16 +0000 (04:56 -0700)
Moved to a single query when showing FastTrack, instead of
previous three.  This change also limits photos so we only
request when needed for large mode, and even then only the
super-primary photo.  Helps fix http://b/2134623

New query also brings in new social status API along with
attribution lines, fixing http://b/2149038

Change also adds any IM rows under contact, or treats
Email entries with presence as Google Talk IM rows, which
fixes http://b/2087402

When selecting chiclet icons, prefer a specific set of
built-in platform and system apps.  This does /not/ replace
the system intent dialog to actually select an app when
there are multiple matches.  Fixes http://b/2139517

Heavy recycling strategies for preserving layouts.  This
uses a ActivityManager trick to keep FastTrack pre-warmed
behind any other tasks.  It also recycles inflated chiclets
between showings.  With these and other optimizations, we
now show 60% faster.  Fixes http://b/2134623

Implements large FastTrack layout, fixing http://b/2126683

Switched import/export icons to correct DPI, corrected HDPI
FastTrack shadow asset, and fixed dialog launching logic in
ContactsListView.

14 files changed:
AndroidManifest.xml
res/drawable-hdpi-finger/ic_menu_import_export.png [moved from res/drawable-mdpi/ic_menu_import_export.png with 100% similarity]
res/drawable-hdpi-finger/quickcontact_drop_shadow.9.png
res/drawable-mdpi-finger/ic_menu_import_export.png [moved from res/drawable-hdpi/ic_menu_import_export.png with 100% similarity]
res/layout-finger/quickcontact.xml
res/layout-finger/quickcontact_header_large.xml
res/layout-finger/quickcontact_header_med.xml
src/com/android/contacts/ContactsListActivity.java
src/com/android/contacts/model/EntityDelta.java
src/com/android/contacts/model/ExternalSource.java
src/com/android/contacts/ui/EditContactActivity.java
src/com/android/contacts/ui/QuickContactActivity.java
src/com/android/contacts/ui/QuickContactWindow.java
src/com/android/contacts/util/Constants.java

index 0950ff0..bd0a769 100644 (file)
              temporary hack until we add better framework support. -->
         <activity
             android:name=".ui.QuickContactActivity"
-            android:theme="@style/FullyTranslucent.QuickContact">
+            android:theme="@style/FullyTranslucent.QuickContact"
+            android:launchMode="singleTop"
+            android:taskAffinity="android.task.fasttrack">
 
             <intent-filter>
                 <action android:name="com.android.contacts.action.QUICK_CONTACT" />
index 5049903..6fafcbe 100644 (file)
Binary files a/res/drawable-hdpi-finger/quickcontact_drop_shadow.9.png and b/res/drawable-hdpi-finger/quickcontact_drop_shadow.9.png differ
index 33007f7..a2a94d4 100644 (file)
@@ -24,7 +24,7 @@
         android:id="@+id/header"
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"
-        android:layout_marginTop="11dip">
+        android:layout_marginTop="10dip">
 
         <ViewStub
             android:id="@+id/header_small"
             android:id="@android:id/list"
             android:layout_width="fill_parent"
             android:layout_height="0dip"
-           android:layout_weight="1"
+            android:layout_weight="1"
             android:background="@color/quickcontact_disambig"
             android:divider="@drawable/quickcontact_disambig_divider"
             android:cacheColorHint="@null" />
index c94f1ad..df01f8a 100644 (file)
     android:gravity="center_vertical"
     android:orientation="horizontal">
 
+    <ImageView
+        android:id="@+id/photo"
+        android:layout_width="50dip"
+        android:layout_height="56dip"
+        android:layout_marginLeft="15dip"
+        android:layout_marginRight="15dip"
+        style="@*android:style/Widget.QuickContactBadge.WindowLarge" />
+
     <LinearLayout
         android:layout_width="0dip"
         android:layout_height="wrap_content"
         android:layout_weight="1"
-        android:layout_marginLeft="8dip"
         android:paddingRight="8dip"
         android:orientation="vertical">
 
             android:layout_height="wrap_content"
             android:singleLine="true"
             android:ellipsize="end"
-            android:textColor="#f000"
-            android:textSize="14dip" />
+            android:textColor="@*android:color/primary_text_light"
+            android:textStyle="bold"
+            android:textSize="18dip" />
 
         <TextView
             android:id="@+id/status"
             android:layout_width="fill_parent"
             android:layout_height="wrap_content"
-            android:maxLines="1"
+            android:singleLine="true"
+            android:ellipsize="end"
+            android:textColor="@*android:color/secondary_text_light"
+            android:textSize="15dip"
+            android:layout_marginTop="-3dip" />
+
+        <TextView
+            android:id="@+id/timestamp"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:singleLine="true"
             android:ellipsize="end"
-            android:textColor="#f999"
-            android:textSize="12dip" />
+            android:textColor="@*android:color/secondary_text_light"
+            android:textSize="12dip"
+            android:layout_marginTop="-2dip" />
 
     </LinearLayout>
 
index 3533502..a5762fd 100644 (file)
     android:id="@+id/header_medium"
     android:layout_width="fill_parent"
     android:layout_height="wrap_content"
-    android:minHeight="45dip"
+    android:minHeight="51dip"
     android:background="@drawable/quickcontact_top_frame"
     android:gravity="center_vertical"
     android:orientation="horizontal">
 
-    <TextView
-        android:id="@+id/status"
+    <LinearLayout
         android:layout_width="0dip"
         android:layout_height="wrap_content"
         android:layout_weight="1"
         android:layout_marginLeft="15dip"
         android:layout_marginRight="15dip"
-        android:singleLine="true"
-        android:ellipsize="end"
-        android:textColor="#f000"
-        android:textSize="15dip" />
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@+id/status"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:singleLine="true"
+            android:ellipsize="end"
+            android:textColor="@*android:color/primary_text_light"
+            android:textSize="15sp" />
+
+        <TextView
+            android:id="@+id/timestamp"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:singleLine="true"
+            android:ellipsize="end"
+            android:textColor="@*android:color/secondary_text_light"
+            android:textSize="12sp"
+            android:layout_marginTop="-2dip" />
+
+    </LinearLayout>
 
     <ImageView
+        android:id="@+id/source_divider"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_marginTop="1dip"
@@ -44,8 +62,8 @@
 
     <ImageView
         android:id="@+id/source"
-        android:layout_width="24dip"
-        android:layout_height="24dip"
+        android:layout_width="30dip"
+        android:layout_height="30dip"
         android:layout_marginLeft="13dip"
         android:layout_marginRight="16dip"
         android:scaleType="centerInside" />
index 5e1638f..5d3f619 100644 (file)
@@ -875,45 +875,44 @@ public class ContactsListActivity extends ListActivity implements
                 return AccountSelectionUtil.getSelectAccountDialog(this, id);
             }
             case R.id.dialog_sdcard_not_found: {
-                AlertDialog.Builder builder = new AlertDialog.Builder(this)
+                return new AlertDialog.Builder(this)
                         .setTitle(R.string.no_sdcard_title)
                         .setIcon(android.R.drawable.ic_dialog_alert)
                         .setMessage(R.string.no_sdcard_message)
-                        .setPositiveButton(android.R.string.ok, null);
-               break;
+                        .setPositiveButton(android.R.string.ok, null).create();
             }
-           case R.id.dialog_delete_contact_confirmation: {
-                AlertDialog.Builder builder = new AlertDialog.Builder(this)
+            case R.id.dialog_delete_contact_confirmation: {
+                return new AlertDialog.Builder(this)
                         .setTitle(R.string.deleteConfirmation_title)
                         .setIcon(android.R.drawable.ic_dialog_alert)
                         .setMessage(R.string.deleteConfirmation)
                         .setNegativeButton(android.R.string.cancel, null)
-                        .setPositiveButton(android.R.string.ok, new DeleteClickListener(mSelectedContactUri));
-                break;
+                        .setPositiveButton(android.R.string.ok,
+                                new DeleteClickListener(mSelectedContactUri)).create();
             }
             case R.id.dialog_cannot_delete_readonly_contact: {
-                AlertDialog.Builder builder = new AlertDialog.Builder(this)
+                return new AlertDialog.Builder(this)
                         .setTitle(R.string.deleteConfirmation_title)
                         .setIcon(android.R.drawable.ic_dialog_alert)
                         .setMessage(R.string.readOnlyContactWarning)
-                        .setPositiveButton(android.R.string.ok, null);
-               break;
+                        .setPositiveButton(android.R.string.ok, null).create();
             }
             case R.id.dialog_readonly_contact_delete_confirmation: {
-                AlertDialog.Builder builder = new AlertDialog.Builder(this)
+                return new AlertDialog.Builder(this)
                         .setTitle(R.string.deleteConfirmation_title)
                         .setIcon(android.R.drawable.ic_dialog_alert)
                         .setMessage(R.string.readOnlyContactDeleteConfirmation)
-                        .setPositiveButton(android.R.string.ok, new DeleteClickListener(mSelectedContactUri));
-                }
-           case R.id.dialog_multiple_contact_delete_confirmation: {
-                AlertDialog.Builder builder = new AlertDialog.Builder(this)
+                        .setPositiveButton(android.R.string.ok,
+                                new DeleteClickListener(mSelectedContactUri)).create();
+            }
+            case R.id.dialog_multiple_contact_delete_confirmation: {
+                return new AlertDialog.Builder(this)
                         .setTitle(R.string.deleteConfirmation_title)
                         .setIcon(android.R.drawable.ic_dialog_alert)
                         .setMessage(R.string.multipleContactDeleteConfirmation)
-                        .setPositiveButton(android.R.string.ok, new DeleteClickListener(mSelectedContactUri));
-                }
-
+                        .setPositiveButton(android.R.string.ok,
+                                new DeleteClickListener(mSelectedContactUri)).create();
+            }
         }
         return super.onCreateDialog(id);
     }
index dfc4fc4..2581480 100644 (file)
@@ -106,7 +106,7 @@ public class EntityDelta implements Parcelable {
         if (local == null) local = new EntityDelta();
 
         if (LOGV) {
-            final Long localVersion = (local.mValues == null) ? -1 : local.mValues
+            final Long localVersion = (local.mValues == null) ? null : local.mValues
                     .getAsLong(RawContacts.VERSION);
             final Long remoteVersion = remote.mValues.getAsLong(RawContacts.VERSION);
             Log.d(TAG, "Re-parenting from original version " + remoteVersion + " to "
index f22aba4..0786e28 100644 (file)
@@ -109,6 +109,8 @@ public class ExternalSource extends ContactsSource {
             if (parser == null) continue;
             inflate(context, parser);
         }
+
+        setInflatedLevel(inflateLevel);
     }
 
     /**
index 7c644f0..e6bb4cf 100644 (file)
@@ -637,7 +637,7 @@ public final class EditContactActivity extends Activity implements View.OnClickL
 
                 } catch (OperationApplicationException e) {
                     // Version consistency failed, re-parent change and try again
-                    Log.w(TAG, "Version consistency failed, re-parenting", e);
+                    Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
                     final EntitySet newState = EntitySet.fromQuery(resolver,
                             target.mQuerySelection, null, null);
                     state = EntitySet.mergeAfter(newState, state);
index 5cd2d18..86dca28 100644 (file)
@@ -22,6 +22,7 @@ import android.graphics.Rect;
 import android.net.Uri;
 import android.os.Bundle;
 import android.provider.ContactsContract.QuickContact;
+import android.util.Log;
 
 /**
  * Stub translucent activity that just shows {@link QuickContactWindow} floating
@@ -30,18 +31,35 @@ import android.provider.ContactsContract.QuickContact;
  */
 public final class QuickContactActivity extends Activity implements
         QuickContactWindow.OnDismissListener {
+    private static final String TAG = "FastTrackActivity";
+
+    static final boolean LOGV = true;
+    static final boolean FORCE_CREATE = false;
+
     private QuickContactWindow mQuickContact;
 
     @Override
     protected void onCreate(Bundle icicle) {
         super.onCreate(icicle);
+        this.onNewIntent(getIntent());
+    }
+
+    @Override
+    public void onNewIntent(Intent intent) {
+        super.onNewIntent(intent);
+
+        if (LOGV) Log.d(TAG, "onNewIntent");
 
         if (QuickContactWindow.TRACE_LAUNCH) {
             android.os.Debug.startMethodTracing(QuickContactWindow.TRACE_TAG);
         }
 
+        if (mQuickContact == null || FORCE_CREATE) {
+            if (LOGV) Log.d(TAG, "Preparing window");
+            mQuickContact = new QuickContactWindow(this, this);
+        }
+
         // Use our local window token for now
-        final Intent intent = getIntent();
         final Uri lookupUri = intent.getData();
         final Bundle extras = intent.getExtras();
 
@@ -50,19 +68,44 @@ public final class QuickContactActivity extends Activity implements
         final int mode = extras.getInt(QuickContact.EXTRA_MODE, QuickContact.MODE_MEDIUM);
         final String[] excludeMimes = extras.getStringArray(QuickContact.EXTRA_EXCLUDE_MIMES);
 
-        mQuickContact = new QuickContactWindow(this, this);
         mQuickContact.show(lookupUri, target, mode, excludeMimes);
     }
 
+    /** {@inheritDoc} */
     @Override
-    protected void onStop() {
-        super.onStop();
+    public void onBackPressed() {
+        if (LOGV) Log.w(TAG, "Unexpected back captured by stub activity");
+        finish();
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        if (LOGV) Log.d(TAG, "onPause");
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        if (LOGV) Log.d(TAG, "onDestroy");
+
         mQuickContact.dismiss();
     }
 
     /** {@inheritDoc} */
     public void onDismiss(QuickContactWindow dialog) {
-        // When dismissed, finish this activity
-        finish();
+        if (LOGV) Log.d(TAG, "onDismiss");
+
+        if (isTaskRoot() && !FORCE_CREATE) {
+            if (LOGV) Log.d(TAG, "Moving task to back");
+
+            // Instead of stopping, simply push this to the back of the stack.
+            // This is only done when running at the top of the stack;
+            // otherwise, we have been launched by someone else so need to
+            // allow the user to go back to the caller.
+            moveTaskToBack(false);
+        } else {
+            finish();
+        }
     }
 }
index 6059079..756dd1e 100644 (file)
@@ -16,6 +16,7 @@
 
 package com.android.contacts.ui;
 
+import com.android.contacts.ContactsUtils;
 import com.android.contacts.R;
 import com.android.contacts.model.ContactsSource;
 import com.android.contacts.model.Sources;
@@ -24,6 +25,8 @@ import com.android.contacts.ui.widget.CheckableImageView;
 import com.android.contacts.util.Constants;
 import com.android.contacts.util.NotifyingAsyncQueryHandler;
 import com.android.internal.policy.PolicyManager;
+import com.google.android.collect.Lists;
+import com.google.android.collect.Sets;
 
 import android.content.ActivityNotFoundException;
 import android.content.ContentValues;
@@ -31,23 +34,34 @@ import android.content.ContentUris;
 import android.content.Context;
 import android.content.EntityIterator;
 import android.content.Intent;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
 import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
+import android.os.Handler;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.QuickContact;
-import android.provider.ContactsContract.Presence;
 import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.StatusUpdates;
 import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Im;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
 import android.provider.SocialContract.Activities;
 import android.text.TextUtils;
+import android.text.format.DateUtils;
 import android.util.Log;
+import android.util.Pool;
+import android.util.Poolable;
+import android.util.PoolableManager;
+import android.util.Pools;
 import android.view.ContextThemeWrapper;
 import android.view.Gravity;
 import android.view.KeyEvent;
@@ -55,11 +69,13 @@ import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.MotionEvent;
+import android.view.VelocityTracker;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewStub;
 import android.view.Window;
 import android.view.WindowManager;
+import android.view.Window.Callback;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.animation.Animation;
 import android.view.animation.AnimationUtils;
@@ -76,19 +92,20 @@ import android.widget.TextView;
 import android.widget.Toast;
 
 import java.lang.ref.SoftReference;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
 
 /**
- * Window that shows fast-track contact details for a specific
- * {@link Contacts#_ID}.
+ * Window that shows QuickContact dialog for a specific {@link Contacts#_ID}.
  */
 public class QuickContactWindow implements Window.Callback,
         NotifyingAsyncQueryHandler.AsyncQueryListener, View.OnClickListener,
-        AbsListView.OnItemClickListener, CompoundButton.OnCheckedChangeListener {
+        AbsListView.OnItemClickListener, CompoundButton.OnCheckedChangeListener, KeyEvent.Callback {
     private static final String TAG = "QuickContactWindow";
 
     /**
@@ -113,15 +130,13 @@ public class QuickContactWindow implements Window.Callback,
     private OnDismissListener mDismissListener;
     private ResolveCache mResolveCache;
 
-    private long mAggId;
+    private Uri mLookupUri;
     private Rect mAnchor;
 
     private int mShadowHeight;
 
-    private boolean mHasSummary = false;
-    private boolean mHasSocial = false;
     private boolean mHasValidSocial = false;
-    private boolean mHasActions = false;
+    private boolean mHasData = false;
     private boolean mMakePrimary = false;
 
     private ImageView mArrowUp;
@@ -139,35 +154,59 @@ public class QuickContactWindow implements Window.Callback,
     private CheckableImageView mLastAction;
     private CheckBox mSetPrimaryCheckBox;
 
+    private int mWindowRecycled = 0;
+    private int mActionRecycled = 0;
+
     /**
      * Set of {@link Action} that are associated with the aggregate currently
-     * displayed by this fast-track window, represented as a map from
-     * {@link String} MIME-type to {@link ActionList}.
+     * displayed by this dialog, represented as a map from {@link String}
+     * MIME-type to {@link ActionList}.
      */
     private ActionMap mActions = new ActionMap();
 
+    /**
+     * Pool of unused {@link CheckableImageView} that have previously been
+     * inflated, and are ready to be recycled through {@link #obtainView()}.
+     */
+    private LinkedList<View> mActionPool = new LinkedList<View>();
+
     private String[] mExcludeMimes;
 
     /**
-     * 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.
+     * Specific MIME-types that should be bumped to the front of the dialog.
+     * Other MIME-types not appearing in this list follow in alphabetic order.
      */
     private static final String[] ORDERED_MIMETYPES = new String[] {
-        Phone.CONTENT_ITEM_TYPE,
-        Contacts.CONTENT_ITEM_TYPE,
-        Constants.MIME_SMS_ADDRESS,
-        Email.CONTENT_ITEM_TYPE,
+            Phone.CONTENT_ITEM_TYPE,
+            Contacts.CONTENT_ITEM_TYPE,
+            Constants.MIME_SMS_ADDRESS,
+            Email.CONTENT_ITEM_TYPE,
     };
 
-    private static final int TOKEN_SUMMARY = 1;
-    private static final int TOKEN_SOCIAL = 2;
-    private static final int TOKEN_DATA = 3;
+    /**
+     * Specific list {@link ApplicationInfo#packageName} of apps that are
+     * prefered <strong>only</strong> for the purposes of default icons when
+     * multiple {@link ResolveInfo} are found to match. This only happens when
+     * the user has not selected a default app yet, and they will still be
+     * presented with the system disambiguation dialog.
+     */
+    private static final HashSet<String> sPreferResolve = Sets.newHashSet(new String[] {
+            "com.android.email",
+            "com.android.calendar",
+            "com.android.contacts",
+            "com.android.mms",
+            "com.android.phone",
+    });
+
+    private static final int TOKEN_DATA = 1;
+
+    static final boolean LOGD = false;
 
     static final boolean TRACE_LAUNCH = false;
     static final String TRACE_TAG = "quickcontact";
 
     /**
-     * Prepare a fast-track window to show in the given {@link Context}.
+     * Prepare a dialog to show in the given {@link Context}.
      */
     public QuickContactWindow(Context context) {
         mContext = new ContextThemeWrapper(context, R.style.QuickContact);
@@ -208,12 +247,13 @@ public class QuickContactWindow implements Window.Callback,
                 return 1.2f - inner * inner;
             }
         });
+
+        mHandler = new NotifyingAsyncQueryHandler(mContext, this);
     }
 
     /**
-     * Prepare a fast-track window to show in the given {@link Context}, and
-     * notify the given {@link OnDismissListener} each time this dialog is
-     * dismissed.
+     * Prepare a dialog to show in the given {@link Context}, and notify the
+     * given {@link OnDismissListener} each time this dialog is dismissed.
      */
     public QuickContactWindow(Context context, OnDismissListener dismissListener) {
         this(context);
@@ -246,10 +286,10 @@ public class QuickContactWindow implements Window.Callback,
     }
 
     /**
-     * Start showing a fast-track window for the given {@link Contacts#_ID}
-     * pointing towards the given location.
+     * Start showing a dialog for the given {@link Contacts#_ID} pointing
+     * towards the given location.
      */
-    public void show(Uri contactUri, Rect anchor, int mode, String[] excludeMimes) {
+    public void show(Uri lookupUri, Rect anchor, int mode, String[] excludeMimes) {
         if (mShowing || mQuerying) {
             Log.w(TAG, "already in process of showing");
             return;
@@ -260,29 +300,61 @@ public class QuickContactWindow implements Window.Callback,
         }
 
         // Prepare header view for requested mode
+        mLookupUri = lookupUri;
+        mAnchor = new Rect(anchor);
         mMode = mode;
-        mHeader = getHeaderView(mode);
         mExcludeMimes = excludeMimes;
 
+        mHeader = getHeaderView(mode);
+
         setHeaderText(R.id.name, R.string.quickcontact_missing_name);
+
         setHeaderText(R.id.status, null);
+        setHeaderText(R.id.timestamp, null);
+
         setHeaderImage(R.id.presence, null);
+        setHeaderImage(R.id.source, null);
 
         mHasValidSocial = false;
-
-        mAggId = ContentUris.parseId(contactUri);
-        mAnchor = new Rect(anchor);
         mQuerying = true;
 
-        Uri aggSummary = ContentUris.withAppendedId(Contacts.CONTENT_URI, mAggId);
-        Uri aggSocial = ContentUris.withAppendedId(Activities.CONTENT_CONTACT_STATUS_URI, mAggId);
-        Uri aggData = Uri.withAppendedPath(aggSummary, Contacts.Data.CONTENT_DIRECTORY);
+        // Start background query for data, but only select photo rows when they
+        // directly match the super-primary PHOTO_ID.
+        final Uri dataUri = getDataUri(lookupUri);
+        mHandler.cancelOperation(TOKEN_DATA);
 
-        // Start data query in background
-        mHandler = new NotifyingAsyncQueryHandler(mContext, this);
-        mHandler.startQuery(TOKEN_SUMMARY, null, aggSummary, SummaryQuery.PROJECTION, null, null, null);
-        mHandler.startQuery(TOKEN_SOCIAL, null, aggSocial, SocialQuery.PROJECTION, null, null, null);
-        mHandler.startQuery(TOKEN_DATA, null, aggData, DataQuery.PROJECTION, null, null, null);
+        // Only request photo data when required by mode
+        if (mMode == QuickContact.MODE_LARGE) {
+            // Select photos, but only super-primary
+            mHandler.startQuery(TOKEN_DATA, null, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
+                    + "!=? OR (" + Data.MIMETYPE + "=? AND " + Data._ID + "=" + Contacts.PHOTO_ID
+                    + ")", new String[] { Photo.CONTENT_ITEM_TYPE, Photo.CONTENT_ITEM_TYPE }, null);
+        } else {
+            // Exclude all photos from cursor
+            mHandler.startQuery(TOKEN_DATA, null, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
+                    + "!=?", new String[] { Photo.CONTENT_ITEM_TYPE }, null);
+        }
+    }
+
+    /**
+     * Build a {@link Uri} into the {@link Data} table for the requested
+     * {@link Contacts#CONTENT_LOOKUP_URI} style {@link Uri}.
+     */
+    private Uri getDataUri(Uri lookupUri) {
+        // TODO: Formalize method of extracting LOOKUP_KEY
+        final List<String> path = lookupUri.getPathSegments();
+        final boolean validLookup = path.size() >= 3 && "lookup".equals(path.get(1));
+        if (!validLookup) {
+            // We only accept valid lookup-style Uris
+            throw new IllegalArgumentException("Expecting lookup-style Uri");
+        } else if (path.size() == 3) {
+            // No direct _ID provided, so force a lookup
+            lookupUri = Contacts.lookupContact(mContext.getContentResolver(), lookupUri);
+        }
+
+        final long contactId = ContentUris.parseId(lookupUri);
+        return Uri.withAppendedPath(ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId),
+                Contacts.Data.CONTENT_DIRECTORY);
     }
 
     /**
@@ -302,7 +374,7 @@ public class QuickContactWindow implements Window.Callback,
     }
 
     /**
-     * Actual internal method to show this fast-track window. Called only by
+     * Actual internal method to show this dialog. Called only by
      * {@link #considerShowing()} when all data requirements have been met.
      */
     private void showInternal() {
@@ -347,66 +419,69 @@ public class QuickContactWindow implements Window.Callback,
 
         if (TRACE_LAUNCH) {
             android.os.Debug.stopMethodTracing();
+            Log.d(TAG, "Window recycled " + mWindowRecycled + " times, chiclets "
+                    + mActionRecycled + " times");
         }
     }
 
     /**
-     * Dismiss this fast-track window if showing.
+     * Dismiss this dialog if showing.
      */
     public void dismiss() {
+        // Notify any listeners that we've been dismissed
+        if (mDismissListener != null) {
+            mDismissListener.onDismiss(this);
+        }
+
         if (!isShowing()) {
-            Log.d(TAG, "not visible, ignore");
+            if (LOGD) Log.d(TAG, "not visible, ignore");
             return;
         }
 
         boolean hadDecor = mDecor != null;
-
         if (hadDecor) {
             mWindowManager.removeView(mDecor);
             mDecor = null;
             mWindow.closeAllPanels();
         }
 
-        // Release refrence to last chiclet.
+        // Release reference to last chiclet.
         mLastAction = null;
 
         // Completely hide header from current mode
         mHeader.setVisibility(View.GONE);
 
         // Cancel any pending queries
-        mHandler.cancelOperation(TOKEN_SUMMARY);
-        mHandler.cancelOperation(TOKEN_SOCIAL);
         mHandler.cancelOperation(TOKEN_DATA);
 
         // Clear track actions and scroll to hard left
         mResolveCache.clear();
         mActions.clear();
-        mTrack.removeViews(1, mTrack.getChildCount() - 2);
+
+        // Recycle any chiclets in use
+        while (mTrack.getChildCount() > 2) {
+            this.releaseView(mTrack.getChildAt(1));
+            mTrack.removeViewAt(1);
+        }
+
         mTrackScroll.fullScroll(View.FOCUS_LEFT);
         mWasDownArrow = false;
 
         setResolveVisible(false, null);
 
         mQuerying = false;
-        mHasSummary = false;
-        mHasSocial = false;
-        mHasActions = false;
 
         if (!hadDecor || !mShowing) {
-            Log.d(TAG, "not showing, ignore");
+            if (LOGD) Log.d(TAG, "not showing, ignore");
             return;
         }
 
         mShowing = false;
-
-        // Notify any listeners that we've been dismissed
-        if (mDismissListener != null) {
-            mDismissListener.onDismiss(this);
-        }
+        mWindowRecycled++;
     }
 
     /**
-     * Returns true if this fast-track window is showing or querying.
+     * Returns true if this dialog is showing or querying.
      */
     public boolean isShowing() {
         return mShowing || mQuerying;
@@ -417,7 +492,7 @@ public class QuickContactWindow implements Window.Callback,
      * {@link #showInternal()} when all data items are present.
      */
     private synchronized void considerShowing() {
-        if (mHasSummary && mHasSocial && mHasActions && !mShowing) {
+        if (mHasData && !mShowing) {
             if (mMode == QuickContact.MODE_MEDIUM && !mHasValidSocial) {
                 // Missing valid social, swap medium for small header
                 mHeader.setVisibility(View.GONE);
@@ -438,20 +513,8 @@ public class QuickContactWindow implements Window.Callback,
             return;
         }
 
-        switch (token) {
-            case TOKEN_SUMMARY:
-                handleSummary(cursor);
-                mHasSummary = true;
-                break;
-            case TOKEN_SOCIAL:
-                handleSocial(cursor);
-                mHasSocial = true;
-                break;
-            case TOKEN_DATA:
-                handleData(cursor);
-                mHasActions = true;
-                break;
-        }
+        handleData(cursor);
+        mHasData = true;
 
         if (!cursor.isClosed()) {
             cursor.close();
@@ -470,6 +533,7 @@ public class QuickContactWindow implements Window.Callback,
         final View view = mHeader.findViewById(id);
         if (view instanceof TextView) {
             ((TextView)view).setText(value);
+            view.setVisibility(TextUtils.isEmpty(value) ? View.GONE : View.VISIBLE);
         }
     }
 
@@ -483,56 +547,28 @@ public class QuickContactWindow implements Window.Callback,
         final View view = mHeader.findViewById(id);
         if (view instanceof ImageView) {
             ((ImageView)view).setImageDrawable(drawable);
+            view.setVisibility(drawable == null ? View.GONE : View.VISIBLE);
         }
     }
 
     /**
-     * Handle the result from the {@link #TOKEN_SUMMARY} query.
-     */
-    private void handleSummary(Cursor cursor) {
-        if (cursor == null || !cursor.moveToNext()) return;
-
-        // TODO: switch to provider-specific presence dots instead of using
-        // overall summary dot.
-        final String name = cursor.getString(SummaryQuery.DISPLAY_NAME);
-        final int status = cursor.getInt(SummaryQuery.CONTACT_PRESENCE);
-        final Drawable statusIcon = getPresenceIcon(status);
-
-        setHeaderText(R.id.name, name);
-        setHeaderImage(R.id.presence, statusIcon);
-    }
-
-    /**
-     * Handle the result from the {@link #TOKEN_SOCIAL} query.
-     */
-    private void handleSocial(Cursor cursor) {
-        if (cursor == null || !cursor.moveToNext()) return;
-
-        final String status = cursor.getString(SocialQuery.TITLE);
-
-        mHasValidSocial = !TextUtils.isEmpty(status);
-
-        setHeaderText(R.id.status, status);
-    }
-
-    /**
      * Find the presence icon for showing in summary header.
      */
     private Drawable getPresenceIcon(int status) {
         int resId = -1;
         switch (status) {
-            case Presence.AVAILABLE:
+            case StatusUpdates.AVAILABLE:
                 resId = android.R.drawable.presence_online;
                 break;
-            case Presence.IDLE:
-            case Presence.AWAY:
+            case StatusUpdates.IDLE:
+            case StatusUpdates.AWAY:
                 resId = android.R.drawable.presence_away;
                 break;
-            case Presence.DO_NOT_DISTURB:
+            case StatusUpdates.DO_NOT_DISTURB:
                 resId = android.R.drawable.presence_busy;
                 break;
         }
-        if (resId > 0) {
+        if (resId != -1) {
             return mContext.getResources().getDrawable(resId);
         } else {
             return null;
@@ -540,25 +576,25 @@ public class QuickContactWindow implements Window.Callback,
     }
 
     /**
-     * Find the Fast-Track-specific presence icon for showing in chiclets.
+     * Find the QuickContact-specific presence icon for showing in chiclets.
      */
     private Drawable getTrackPresenceIcon(int status) {
         int resId = -1;
         switch (status) {
-            case Presence.AVAILABLE:
+            case StatusUpdates.AVAILABLE:
                 resId = R.drawable.quickcontact_slider_presence_active;
                 break;
-            case Presence.IDLE:
-            case Presence.AWAY:
+            case StatusUpdates.IDLE:
+            case StatusUpdates.AWAY:
                 resId = R.drawable.quickcontact_slider_presence_away;
                 break;
-            case Presence.DO_NOT_DISTURB:
+            case StatusUpdates.DO_NOT_DISTURB:
                 resId = R.drawable.quickcontact_slider_presence_busy;
                 break;
-            case Presence.INVISIBLE:
+            case StatusUpdates.INVISIBLE:
                 resId = R.drawable.quickcontact_slider_presence_inactive;
                 break;
-            case Presence.OFFLINE:
+            case StatusUpdates.OFFLINE:
             default:
                 resId = R.drawable.quickcontact_slider_presence_inactive;
         }
@@ -571,7 +607,7 @@ public class QuickContactWindow implements Window.Callback,
         return cursor.getString(index);
     }
 
-    /** Read {@link int} from the given {@link Cursor}. */
+    /** Read {@link Integer} from the given {@link Cursor}. */
     private static int getAsInt(Cursor cursor, String columnName) {
         final int index = cursor.getColumnIndex(columnName);
         return cursor.getInt(index);
@@ -602,7 +638,6 @@ public class QuickContactWindow implements Window.Callback,
          * Returns a lookup (@link Uri) for the contact data item.
          */
         public Uri getDataUri();
-
     }
 
     /**
@@ -671,11 +706,34 @@ public class QuickContactWindow implements Window.Callback,
                     mIntent = new Intent(Intent.ACTION_SENDTO, mailUri);
                 }
 
-            } else {
+            } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                final boolean isEmail = Email.CONTENT_ITEM_TYPE.equals(
+                        getAsString(cursor, Data.MIMETYPE));
+                final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK :
+                        getAsInt(cursor, Im.PROTOCOL);
+
+                String host = getAsString(cursor, Im.CUSTOM_PROTOCOL);
+                String data = getAsString(cursor, isEmail ? Email.DATA : Im.DATA);
+                if (protocol != Im.PROTOCOL_CUSTOM) {
+                    // Try bringing in a well-known host for specific protocols
+                    host = ContactsUtils.lookupProviderNameFromId(protocol);
+                }
+
+                if (!TextUtils.isEmpty(host) && !TextUtils.isEmpty(data)) {
+                    final String authority = host.toLowerCase();
+                    final Uri imUri = new Uri.Builder().scheme(Constants.SCHEME_IMTO).authority(
+                            authority).appendPath(data).build();
+                    mIntent = new Intent(Intent.ACTION_SENDTO, imUri);
+                }
+            }
+
+            if (mIntent == null) {
                 // Otherwise fall back to default VIEW action
                 mIntent = new Intent(Intent.ACTION_VIEW, mDataUri);
             }
-           mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+            // Always launch as new task, since we're like a launcher
+            mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         }
 
         /** {@inheritDoc} */
@@ -730,11 +788,11 @@ public class QuickContactWindow implements Window.Callback,
      */
     private static class ProfileAction implements Action {
         private final Context mContext;
-        private final long mId;
+        private final Uri mLookupUri;
 
-        public ProfileAction(Context context, long contactId) {
+        public ProfileAction(Context context, Uri lookupUri) {
             mContext = context;
-            mId = contactId;
+            mLookupUri = lookupUri;
         }
 
         /** {@inheritDoc} */
@@ -759,8 +817,7 @@ public class QuickContactWindow implements Window.Callback,
 
         /** {@inheritDoc} */
         public Intent getIntent() {
-            final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, mId);
-            final Intent intent = new Intent(Intent.ACTION_VIEW, contactUri);
+            final Intent intent = new Intent(Intent.ACTION_VIEW, mLookupUri);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            return intent;
         }
@@ -816,8 +873,16 @@ public class QuickContactWindow implements Window.Callback,
                 final List<ResolveInfo> matches = mPackageManager.queryIntentActivities(intent,
                         PackageManager.MATCH_DEFAULT_ONLY);
 
-                if (matches.size() > 0) {
-                    final ResolveInfo bestResolve = matches.get(0);
+                // Pick first match, otherwise best found
+                ResolveInfo bestResolve = null;
+                final int size = matches.size();
+                if (size == 1) {
+                    bestResolve = matches.get(0);
+                } else if (size > 1) {
+                    bestResolve = getBestResolve(matches);
+                }
+
+                if (bestResolve != null) {
                     final Drawable icon = bestResolve.loadIcon(mPackageManager);
 
                     entry.bestResolve = bestResolve;
@@ -830,6 +895,32 @@ public class QuickContactWindow implements Window.Callback,
         }
 
         /**
+         * Best {@link ResolveInfo} when multiple found. Ties are broken by
+         * selecting first from the {QuickContactWindow#sPreferResolve} list of
+         * preferred packages, second by apps that live on the system partition,
+         * otherwise the app from the top of the list. This is
+         * <strong>only</strong> used for selecting a default icon for
+         * displaying in the track, and does not shortcut the system
+         * {@link Intent} disambiguation dialog.
+         */
+        protected ResolveInfo getBestResolve(List<ResolveInfo> matches) {
+            // Accept any package from prefer list, otherwise first system app
+            ResolveInfo firstSystem = null;
+            for (ResolveInfo info : matches) {
+                final boolean isSystem = (info.activityInfo.applicationInfo.flags
+                        & ApplicationInfo.FLAG_SYSTEM) != 0;
+                final boolean isPrefer = QuickContactWindow.sPreferResolve
+                        .contains(info.activityInfo.applicationInfo.packageName);
+
+                if (isPrefer) return info;
+                if (isSystem && firstSystem != null) firstSystem = info;
+            }
+
+            // Return first system found, otherwise first from list
+            return firstSystem != null ? firstSystem : matches.get(0);
+        }
+
+        /**
          * Check {@link PackageManager} to see if any apps offer to handle the
          * given {@link Action}.
          */
@@ -906,6 +997,99 @@ public class QuickContactWindow implements Window.Callback,
     }
 
     /**
+     * Internal storage for the latest social status, as built when walking
+     * across a {@Link DataQuery} query. Will always keep record of at
+     * least the first status it encounters, but will replace it with newer
+     * statuses, as determined by timestamps.
+     */
+    private static class LatestStatus {
+        private String mStatus = null;
+        private long mTimestamp = -1;
+
+        private String mResPackage = null;
+        private int mIconRes = -1;
+        private int mLabelRes = -1;
+
+        private int getCursorInt(Cursor cursor, int columnIndex, int missingValue) {
+            if (cursor.isNull(columnIndex)) return missingValue;
+            return cursor.getInt(columnIndex);
+        }
+
+        /**
+         * Attempt updating this {@link LatestStatus} based on values at the
+         * current row of the given {@link Cursor}. Assumes that query
+         * projection was {@link DataQuery#PROJECTION}.
+         */
+        public void possibleUpdate(Cursor cursor) {
+            final boolean hasStatus = !cursor.isNull(DataQuery.STATUS);
+            final boolean hasTimestamp = !cursor.isNull(DataQuery.STATUS_TIMESTAMP);
+
+            // Bail early when not valid status, or when previous status was
+            // found and we can't compare this one.
+            if (!hasStatus) return;
+            if (isValid() && !hasTimestamp) return;
+
+            if (hasTimestamp) {
+                // Compare timestamps and bail if older status
+                final long newTimestamp = cursor.getLong(DataQuery.STATUS_TIMESTAMP);
+                if (newTimestamp < mTimestamp) return;
+
+                mTimestamp = newTimestamp;
+            }
+
+            // Fill in remaining details from cursor
+            mStatus = cursor.getString(DataQuery.STATUS);
+            mResPackage = cursor.getString(DataQuery.STATUS_RES_PACKAGE);
+            mIconRes = getCursorInt(cursor, DataQuery.STATUS_ICON, -1);
+            mLabelRes = getCursorInt(cursor, DataQuery.STATUS_LABEL, -1);
+        }
+
+        public boolean isValid() {
+            return !TextUtils.isEmpty(mStatus);
+        }
+
+        public CharSequence getStatus() {
+            return mStatus;
+        }
+
+        /**
+         * Build any timestamp and label into a single string.
+         */
+        public CharSequence getTimestampLabel(Context context) {
+            final PackageManager pm = context.getPackageManager();
+
+            final boolean validTimestamp = mTimestamp > 0;
+            final boolean validLabel = mResPackage != null && mLabelRes != -1;
+
+            final CharSequence timeClause = validTimestamp ? DateUtils.getRelativeTimeSpanString(
+                    mTimestamp, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS,
+                    DateUtils.FORMAT_ABBREV_RELATIVE) : null;
+            final CharSequence labelClause = validLabel ? pm.getText(mResPackage, mLabelRes,
+                    null) : null;
+
+            if (validTimestamp && validLabel) {
+                return context.getString(
+                        com.android.internal.R.string.contact_status_update_attribution_with_date,
+                        timeClause, labelClause);
+            } else if (validLabel) {
+                return context.getString(
+                        com.android.internal.R.string.contact_status_update_attribution,
+                        labelClause);
+            } else if (validTimestamp) {
+                return timeClause;
+            } else {
+                return null;
+            }
+        }
+
+        public Drawable getIcon(Context context) {
+            final PackageManager pm = context.getPackageManager();
+            final boolean validIcon = mResPackage != null && mIconRes != -1;
+            return validIcon ? pm.getDrawable(mResPackage, mIconRes, null) : null;
+        }
+    }
+
+    /**
      * Handle the result from the {@link #TOKEN_DATA} query.
      */
     private void handleData(Cursor cursor) {
@@ -913,37 +1097,36 @@ public class QuickContactWindow implements Window.Callback,
 
         if (!isMimeExcluded(Contacts.CONTENT_ITEM_TYPE)) {
             // Add the profile shortcut action
-            final Action action = new ProfileAction(mContext, mAggId);
+            final Action action = new ProfileAction(mContext, mLookupUri);
             mActions.collect(Contacts.CONTENT_ITEM_TYPE, action);
         }
 
+        final LatestStatus status = new LatestStatus();
         final Sources sources = Sources.getInstance(mContext);
         final ImageView photoView = (ImageView)mHeader.findViewById(R.id.photo);
 
+        Bitmap photoBitmap = null;
         while (cursor.moveToNext()) {
+            final long dataId = cursor.getLong(DataQuery._ID);
             final String accountType = cursor.getString(DataQuery.ACCOUNT_TYPE);
             final String resPackage = cursor.getString(DataQuery.RES_PACKAGE);
             final String mimeType = cursor.getString(DataQuery.MIMETYPE);
-            final long dataId = cursor.getLong(DataQuery._ID);
+
+            // Handle any social status updates from this row
+            status.possibleUpdate(cursor);
 
             // Skip this data item if MIME-type excluded
             if (isMimeExcluded(mimeType)) continue;
 
-            // Handle when a photo appears in the various data items
-            // TODO: accept a photo only if its marked as primary
-            // TODO: move to using photo thumbnail columns, when they exist
-            // TODO: launch photo as separate TOKEN query, only for large
-//            if (photoView != null && Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
-//                final int colPhoto = cursor.getColumnIndex(Photo.PHOTO);
-//                final byte[] photoBlob = cursor.getBlob(colPhoto);
-//                final Bitmap photoBitmap = BitmapFactory.decodeByteArray(photoBlob, 0,
-//                        photoBlob.length);
-//                photoView.setImageBitmap(photoBitmap);
-//                continue;
-//            }
-
-            // TODO: find the ContactsSource for this, either from accountType,
-            // or through lazy-loading when resPackage is set, or default.
+            // Handle photos included as data row
+            if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                final int colPhoto = cursor.getColumnIndex(Photo.PHOTO);
+                final byte[] photoBlob = cursor.getBlob(colPhoto);
+                if (photoBlob != null) {
+                    photoBitmap = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
+                }
+                continue;
+            }
 
             final DataKind kind = sources.getKindOrFallback(accountType, mimeType, mContext,
                     ContactsSource.LEVEL_MIMETYPES);
@@ -962,6 +1145,51 @@ public class QuickContactWindow implements Window.Callback,
                         kind, dataId, cursor);
                 considerAdd(action, Constants.MIME_SMS_ADDRESS);
             }
+
+            // Handle Email rows with presence data as Im entry
+            final boolean hasPresence = !cursor.isNull(DataQuery.PRESENCE);
+            if (hasPresence && Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                final DataKind imKind = sources.getKindOrFallback(accountType,
+                        Im.CONTENT_ITEM_TYPE, mContext, ContactsSource.LEVEL_MIMETYPES);
+                if (imKind != null) {
+                    final Action action = new DataAction(mContext, Im.CONTENT_ITEM_TYPE, imKind,
+                            dataId, cursor);
+                    considerAdd(action, Im.CONTENT_ITEM_TYPE);
+                }
+            }
+        }
+
+        if (cursor.moveToLast()) {
+            // Read contact information from last data row
+            final String name = cursor.getString(DataQuery.DISPLAY_NAME);
+            final int presence = cursor.getInt(DataQuery.CONTACT_PRESENCE);
+            final Drawable statusIcon = getPresenceIcon(presence);
+
+            setHeaderText(R.id.name, name);
+            setHeaderImage(R.id.presence, statusIcon);
+        }
+
+        if (photoView != null) {
+            // Place photo when discovered in data, otherwise hide
+            photoView.setVisibility(photoBitmap != null ? View.VISIBLE : View.GONE);
+            photoView.setImageBitmap(photoBitmap);
+        }
+
+        mHasValidSocial = status.isValid();
+        if (mHasValidSocial && mMode != QuickContact.MODE_SMALL) {
+            // Update status when valid was found
+            setHeaderText(R.id.status, status.getStatus());
+            setHeaderText(R.id.timestamp, status.getTimestampLabel(mContext));
+
+            final Drawable icon = status.getIcon(mContext);
+            setHeaderImage(R.id.source, icon);
+
+            if (mMode == QuickContact.MODE_MEDIUM) {
+                // Hide medium divider when missing icon
+                final boolean validIcon = icon != null;
+                mHeader.findViewById(R.id.source_divider).setVisibility(
+                        validIcon ? View.VISIBLE : View.GONE);
+            }
         }
 
         // Turn our list of actions into UI elements, starting with common types
@@ -995,12 +1223,34 @@ public class QuickContactWindow implements Window.Callback,
     }
 
     /**
+     * Obtain a new {@link CheckableImageView} for a new chiclet, either by
+     * recycling one from {@link #mActionPool}, or by inflating a new one. When
+     * finished, use {@link #releaseView(View)} to return back into the pool for
+     * later recycling.
+     */
+    private synchronized View obtainView() {
+        View view = mActionPool.poll();
+        if (view == null || QuickContactActivity.FORCE_CREATE) {
+            view = mInflater.inflate(R.layout.quickcontact_item, mTrack, false);
+        }
+        return view;
+    }
+
+    /**
+     * Return the given {@link CheckableImageView} into our internal pool for
+     * possible recycling during another pass.
+     */
+    private synchronized void releaseView(View view) {
+        mActionPool.offer(view);
+        mActionRecycled++;
+    }
+
+    /**
      * Inflate the in-track view for the action of the given MIME-type. Will use
      * the icon provided by the {@link DataKind}.
      */
     private View inflateAction(String mimeType) {
-        CheckableImageView view = (CheckableImageView)mInflater.inflate(
-                R.layout.quickcontact_item, mTrack, false);
+        final CheckableImageView view = (CheckableImageView)obtainView();
         boolean isActionSet = false;
 
         // Add direct intent if single child, otherwise flag for multiple
@@ -1009,12 +1259,12 @@ public class QuickContactWindow implements Window.Callback,
         if (children.size() == 1) {
             view.setTag(firstInfo);
         } else {
-            for (Action action: children) {
-              if (action.isPrimary()) {
-                  view.setTag(action);
-                  isActionSet = true;
-                  break;
-              }
+            for (Action action : children) {
+                if (action.isPrimary()) {
+                    view.setTag(action);
+                    isActionSet = true;
+                    break;
+                }
             }
             if (!isActionSet) {
                 view.setTag(children);
@@ -1024,6 +1274,7 @@ public class QuickContactWindow implements Window.Callback,
         // Set icon and listen for clicks
         final CharSequence descrip = mResolveCache.getDescription(firstInfo);
         final Drawable icon = mResolveCache.getIcon(firstInfo);
+        view.setChecked(false);
         view.setContentDescription(descrip);
         view.setImageDrawable(icon);
         view.setOnClickListener(this);
@@ -1038,8 +1289,8 @@ public class QuickContactWindow implements Window.Callback,
 
     /**
      * Flag indicating if {@link #mArrowDown} was visible during the last call
-     * to {@link #setResolveVisible(boolean)}. Used to decide during a later
-     * call if the arrow should be restored.
+     * to {@link #setResolveVisible(boolean, CheckableImageView)}. Used to
+     * decide during a later call if the arrow should be restored.
      */
     private boolean mWasDownArrow = false;
 
@@ -1151,20 +1402,54 @@ public class QuickContactWindow implements Window.Callback,
         mMakePrimary = isChecked;
     }
 
+    private void onBackPressed() {
+        // Back key will first dismiss any expanded resolve list, otherwise
+        // it will close the entire dialog.
+        if (mFooterDisambig.getVisibility() == View.VISIBLE) {
+            setResolveVisible(false, null);
+        } else {
+            dismiss();
+        }
+    }
+
     /** {@inheritDoc} */
     public boolean dispatchKeyEvent(KeyEvent event) {
-        if (event.getAction() == KeyEvent.ACTION_DOWN
-                && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
-            // Back key will first dismiss any expanded resolve list, otherwise
-            // it will close the entire dialog.
-            if (mFooterDisambig.getVisibility() == View.VISIBLE) {
-                setResolveVisible(false, null);
-            } else {
-                dismiss();
-            }
+        if (mWindow.superDispatchKeyEvent(event)) {
+            return true;
+        }
+        return event.dispatch(this, mDecor != null
+                ? mDecor.getKeyDispatcherState() : null, this);
+    }
+
+    /** {@inheritDoc} */
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        if (keyCode == KeyEvent.KEYCODE_BACK) {
+            event.startTracking();
             return true;
         }
-        return mWindow.superDispatchKeyEvent(event);
+
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking()
+                && !event.isCanceled()) {
+            onBackPressed();
+            return true;
+        }
+
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
+        return false;
     }
 
     /** {@inheritDoc} */
@@ -1181,8 +1466,8 @@ public class QuickContactWindow implements Window.Callback,
         if (event.getAction() == MotionEvent.ACTION_DOWN) {
             // Only try detecting outside events on down-press
             mDecor.getHitRect(mRect);
-           mRect.top = mRect.top + mDecor.getPaddingTop();
-           mRect.bottom = mRect.bottom - mDecor.getPaddingBottom();
+            mRect.top = mRect.top + mDecor.getPaddingTop();
+            mRect.bottom = mRect.bottom - mDecor.getPaddingBottom();
             final int x = (int)event.getX();
             final int y = (int)event.getY();
             if (!mRect.contains(x, y)) {
@@ -1197,8 +1482,9 @@ public class QuickContactWindow implements Window.Callback,
         if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
             dismiss();
             return true;
+        } else {
+            return mWindow.superDispatchTouchEvent(event);
         }
-        return mWindow.superDispatchTouchEvent(event);
     }
 
     /** {@inheritDoc} */
@@ -1270,49 +1556,50 @@ public class QuickContactWindow implements Window.Callback,
         // No actions
     }
 
-    private interface SummaryQuery {
+    private interface DataQuery {
         final String[] PROJECTION = new String[] {
+                Data._ID,
+
+                RawContacts.ACCOUNT_TYPE,
+                Contacts.STARRED,
                 Contacts.DISPLAY_NAME,
-                Contacts.PHOTO_ID,
                 Contacts.CONTACT_PRESENCE,
-                Contacts.CONTACT_STATUS,
-        };
-
-        final int DISPLAY_NAME = 0;
-        final int PHOTO_ID = 1;
-        final int CONTACT_PRESENCE = 2;
-        final int CONTACT_STATUS = 3;
-    }
-
-    private interface SocialQuery {
-        final String[] PROJECTION = new String[] {
-                Activities.PUBLISHED,
-                Activities.TITLE,
-        };
 
-        final int PUBLISHED = 0;
-        final int TITLE = 1;
-    }
+                Data.STATUS,
+                Data.STATUS_RES_PACKAGE,
+                Data.STATUS_ICON,
+                Data.STATUS_LABEL,
+                Data.STATUS_TIMESTAMP,
+                Data.PRESENCE,
 
-    private interface DataQuery {
-        final String[] PROJECTION = new String[] {
-                Data._ID,
-                RawContacts.ACCOUNT_TYPE,
                 Data.RES_PACKAGE,
                 Data.MIMETYPE,
                 Data.IS_PRIMARY,
                 Data.IS_SUPER_PRIMARY,
                 Data.RAW_CONTACT_ID,
+
                 Data.DATA1, Data.DATA2, Data.DATA3, Data.DATA4, Data.DATA5,
                 Data.DATA6, Data.DATA7, Data.DATA8, Data.DATA9, Data.DATA10, Data.DATA11,
                 Data.DATA12, Data.DATA13, Data.DATA14, Data.DATA15,
         };
 
         final int _ID = 0;
+
         final int ACCOUNT_TYPE = 1;
-        final int RES_PACKAGE = 2;
-        final int MIMETYPE = 3;
-        final int IS_PRIMARY = 4;
-        final int IS_SUPER_PRIMARY = 5;
+        final int STARRED = 2;
+        final int DISPLAY_NAME = 3;
+        final int CONTACT_PRESENCE = 4;
+
+        final int STATUS = 5;
+        final int STATUS_RES_PACKAGE = 6;
+        final int STATUS_ICON = 7;
+        final int STATUS_LABEL = 8;
+        final int STATUS_TIMESTAMP = 9;
+        final int PRESENCE = 10;
+
+        final int RES_PACKAGE = 11;
+        final int MIMETYPE = 12;
+        final int IS_PRIMARY = 13;
+        final int IS_SUPER_PRIMARY = 14;
     }
 }
index 696717e..e0178ad 100644 (file)
@@ -34,5 +34,6 @@ public class Constants {
     public static final String SCHEME_TEL = "tel";
     public static final String SCHEME_SMSTO = "smsto";
     public static final String SCHEME_MAILTO = "mailto";
+    public static final String SCHEME_IMTO = "imto";
 
 }