Merge "revive contacts launch performance tests & also add the test for dialer UI" into froyo
diff --git a/res/layout-finger/contacts_list_item.xml b/res/layout-finger/contacts_list_item.xml
deleted file mode 100644
index 67de04d..0000000
--- a/res/layout-finger/contacts_list_item.xml
+++ /dev/null
@@ -1,91 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
- * Copyright 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.
- */
--->
-
-<LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:orientation="vertical"
->
-    <include
-        android:id="@+id/header"
-        layout="@layout/list_section"
-    />
-
-    <RelativeLayout
-        android:layout_width="match_parent"
-        android:layout_height="?android:attr/listPreferredItemHeight"
-        android:paddingLeft="14dip"
-    >
-
-        <include
-            android:id="@+id/right_side"
-            layout="@layout/contacts_list_item_presence_and_action"
-        />
-
-        <TextView android:id="@+id/label"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_alignParentLeft="true"
-            android:layout_alignParentBottom="true"
-            android:layout_marginBottom="8dip"
-            android:layout_marginTop="-8dip"
-
-            android:singleLine="true"
-            android:ellipsize="marquee"
-            android:textAppearance="?android:attr/textAppearanceSmall"
-            android:textStyle="bold"
-        />
-
-        <TextView android:id="@+id/data"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginLeft="5dip"
-            android:layout_toRightOf="@id/label"
-            android:layout_toLeftOf="@id/right_side"
-            android:layout_alignBaseline="@id/label"
-            android:layout_alignWithParentIfMissing="true"
-
-            android:singleLine="true"
-            android:ellipsize="marquee"
-            android:textAppearance="?android:attr/textAppearanceSmall"
-        />
-
-        <TextView android:id="@+id/name"
-            android:layout_width="0dip"
-            android:layout_height="0dip"
-            android:layout_alignParentLeft="true"
-            android:layout_marginBottom="1dip"
-            android:layout_toLeftOf="@id/right_side"
-            android:layout_alignParentTop="true"
-            android:layout_above="@id/label"
-            android:layout_alignWithParentIfMissing="true"
-
-            android:singleLine="true"
-            android:ellipsize="marquee"
-            android:gravity="center_vertical|left"
-            android:textAppearance="?android:attr/textAppearanceLarge"
-        />
-    </RelativeLayout>
-
-    <View android:id="@+id/list_divider"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:background="@*android:drawable/divider_horizontal_dark_opaque"
-    />
-</LinearLayout>
diff --git a/res/layout-finger/contacts_list_item_photo.xml b/res/layout-finger/contacts_list_item_photo.xml
deleted file mode 100644
index cad1bb9..0000000
--- a/res/layout-finger/contacts_list_item_photo.xml
+++ /dev/null
@@ -1,105 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
- * Copyright 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.
- */
--->
-
-<LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:orientation="vertical"
->
-    <include
-        android:id="@+id/header"
-        layout="@layout/list_section"
-    />
-
-    <RelativeLayout
-        android:layout_width="match_parent"
-        android:layout_height="?android:attr/listPreferredItemHeight"
-        android:paddingLeft="4dip"
-    >
-
-        <include
-            android:id="@+id/right_side"
-            layout="@layout/contacts_list_item_presence_and_action"
-        />
-
-        <android.widget.QuickContactBadge android:id="@+id/photo"
-            android:layout_alignParentLeft="true"
-            android:layout_centerVertical="true"
-            android:layout_marginRight="8dip"
-            style="?android:attr/quickContactBadgeStyleWindowMedium"
-        />
-
-        <ImageView android:id="@+id/noQuickContactPhoto"
-            android:layout_alignParentLeft="true"
-            android:layout_centerVertical="true"
-            android:layout_marginRight="8dip"
-            android:background="@null"
-            style="?android:attr/quickContactBadgeStyleWindowMedium"
-        />
-
-        <TextView android:id="@+id/label"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_toRightOf="@id/photo"
-            android:layout_alignParentBottom="true"
-            android:layout_marginBottom="8dip"
-            android:layout_marginTop="-10dip"
-
-            android:singleLine="true"
-            android:ellipsize="marquee"
-            android:textAppearance="?android:attr/textAppearanceSmall"
-            android:textStyle="bold"
-        />
-
-        <TextView android:id="@+id/data"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginLeft="5dip"
-            android:layout_toRightOf="@id/label"
-            android:layout_toLeftOf="@id/right_side"
-            android:layout_alignBaseline="@id/label"
-            android:layout_alignWithParentIfMissing="true"
-
-            android:singleLine="true"
-            android:ellipsize="marquee"
-            android:textAppearance="?android:attr/textAppearanceSmall"
-        />
-
-        <TextView android:id="@+id/name"
-            android:layout_width="0dip"
-            android:layout_height="0dip"
-            android:layout_toRightOf="@id/photo"
-            android:layout_toLeftOf="@id/right_side"
-            android:layout_alignParentTop="true"
-            android:layout_above="@id/label"
-            android:layout_alignWithParentIfMissing="true"
-
-            android:singleLine="true"
-            android:ellipsize="marquee"
-            android:gravity="center_vertical|left"
-            android:textAppearance="?android:attr/textAppearanceLarge"
-        />
-    </RelativeLayout>
-
-    <View android:id="@+id/list_divider"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:background="@*android:drawable/divider_horizontal_dark_opaque"
-    />
-</LinearLayout>
diff --git a/res/layout-finger/contacts_list_item_photo_and_snippet.xml b/res/layout-finger/contacts_list_item_photo_and_snippet.xml
deleted file mode 100644
index 74e8f7e..0000000
--- a/res/layout-finger/contacts_list_item_photo_and_snippet.xml
+++ /dev/null
@@ -1,127 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
- * Copyright 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.
- */
--->
-
-<LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:orientation="vertical"
->
-    <include
-        android:id="@+id/header"
-        layout="@layout/list_section"
-    />
-
-    <RelativeLayout
-        android:layout_width="match_parent"
-        android:layout_height="?android:attr/listPreferredItemHeight"
-        android:paddingLeft="4dip"
-    >
-
-        <include
-            android:id="@+id/right_side"
-            layout="@layout/contacts_list_item_presence_and_action"
-        />
-
-        <android.widget.QuickContactBadge android:id="@+id/photo"
-            android:layout_alignParentLeft="true"
-            android:layout_centerVertical="true"
-            android:layout_marginRight="8dip"
-            style="?android:attr/quickContactBadgeStyleWindowMedium"
-        />
-
-        <ImageView android:id="@+id/noQuickContactPhoto"
-            android:layout_alignParentLeft="true"
-            android:layout_centerVertical="true"
-            android:layout_marginRight="8dip"
-            android:background="@null"
-            style="?android:attr/quickContactBadgeStyleWindowMedium"
-        />
-
-        <TextView android:id="@+id/label"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_toRightOf="@id/photo"
-            android:layout_alignParentBottom="true"
-            android:layout_marginBottom="3dip"
-            android:layout_marginTop="-7dip"
-
-            android:singleLine="true"
-            android:ellipsize="marquee"
-            android:textAppearance="?android:attr/textAppearanceSmall"
-            android:textStyle="bold"
-        />
-
-        <TextView android:id="@+id/data"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginLeft="5dip"
-            android:layout_toRightOf="@id/label"
-            android:layout_toLeftOf="@id/right_side"
-            android:layout_alignBaseline="@id/label"
-            android:layout_alignWithParentIfMissing="true"
-
-            android:singleLine="true"
-            android:ellipsize="marquee"
-            android:textAppearance="?android:attr/textAppearanceSmall"
-        />
-
-        <RelativeLayout
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_toRightOf="@id/photo"
-            android:layout_toLeftOf="@id/right_side"
-            android:layout_above="@id/label"
-            android:layout_alignParentTop="true"
-            android:layout_alignWithParentIfMissing="true"
-            android:gravity="center_vertical|left"
-            >
-
-            <TextView android:id="@+id/name"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_alignParentTop="true"
-
-                android:singleLine="true"
-                android:ellipsize="marquee"
-                android:textAppearance="?android:attr/textAppearanceLarge"
-            />
-
-            <TextView android:id="@+id/snippet"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_below="@id/name"
-                android:layout_alignWithParentIfMissing="true"
-                android:layout_marginTop="-5dip"
-                android:layout_marginBottom="3dip"
-
-                android:singleLine="true"
-                android:ellipsize="marquee"
-                android:textAppearance="?android:attr/textAppearanceSmall"
-                android:textStyle="bold"
-                
-            />
-        </RelativeLayout>
-    </RelativeLayout>
-
-    <View android:id="@+id/list_divider"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:background="@*android:drawable/divider_horizontal_dark_opaque"
-    />
-</LinearLayout>
diff --git a/res/layout-finger/contacts_list_item_presence_and_action.xml b/res/layout-finger/contacts_list_item_presence_and_action.xml
deleted file mode 100644
index 80b275f..0000000
--- a/res/layout-finger/contacts_list_item_presence_and_action.xml
+++ /dev/null
@@ -1,64 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
- * Copyright 2010, 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.
- */
--->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="wrap_content"
-    android:layout_height="match_parent"
-    android:orientation="horizontal"
-    android:layout_marginLeft="11dip"
-    android:layout_alignParentRight="true">
-
-    <ImageView android:id="@+id/presence"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginLeft="5dip"
-        android:layout_marginRight="5dip"
-        android:padding="7dip"
-        android:layout_gravity="center_vertical"
-        android:scaleType="centerInside"
-    />
-
-    <LinearLayout android:id="@+id/call_view"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:orientation="horizontal">
-
-        <View android:id="@+id/divider"
-            android:layout_width="1px"
-            android:layout_height="match_parent"
-            android:layout_marginTop="5dip"
-            android:layout_marginBottom="5dip"
-            android:background="@drawable/divider_vertical_dark"
-        />
-
-        <view
-            class="com.android.contacts.ui.widget.DontPressWithParentImageView"
-            android:id="@+id/call_button"
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:paddingLeft="14dip"
-            android:paddingRight="14dip"
-            android:layout_centerVertical="true"
-            android:gravity="center"
-            android:src="@android:drawable/sym_action_call"
-            android:background="@drawable/call_background"
-        />
-
-    </LinearLayout>
-</LinearLayout>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 25189db..4a7a743 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -27,4 +27,16 @@
     
     <dimen name="contact_shortcut_frame_width">50dip</dimen>    
     <dimen name="contact_shortcut_frame_height">56dip</dimen>    
+    
+    <!-- Dimensions for a list item -->
+    <dimen name="list_item_padding_top">4dip</dimen>    
+    <dimen name="list_item_padding_right">11dip</dimen>    
+    <dimen name="list_item_padding_bottom">4dip</dimen>    
+    <dimen name="list_item_padding_left">4dip</dimen>    
+    <dimen name="list_item_gap_between_image_and_text">8dip</dimen>    
+    <dimen name="list_item_gap_between_label_and_data">5dip</dimen>    
+    <dimen name="list_item_call_button_padding">14dip</dimen>    
+    <dimen name="list_item_vertical_divider_margin">5dip</dimen>    
+    <dimen name="list_item_presence_icon_margin">5dip</dimen>    
+    <dimen name="list_item_header_text_width">56dip</dimen>    
 </resources>
diff --git a/src/com/android/contacts/ContactListItemView.java b/src/com/android/contacts/ContactListItemView.java
new file mode 100644
index 0000000..5c6c149
--- /dev/null
+++ b/src/com/android/contacts/ContactListItemView.java
@@ -0,0 +1,587 @@
+/*
+ * Copyright (C) 2010 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.ui.widget.DontPressWithParentImageView;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.provider.ContactsContract.Contacts;
+import android.text.TextUtils;
+import android.text.TextUtils.TruncateAt;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+import android.widget.ImageView.ScaleType;
+
+/**
+ * A custom view for an item in the contact list.
+ */
+public class ContactListItemView extends ViewGroup {
+
+    private static final int QUICK_CONTACT_BADGE_STYLE =
+            com.android.internal.R.attr.quickContactBadgeStyleWindowMedium;
+
+    private final Context mContext;
+
+    private final int mPreferredHeight;
+    private final int mVerticalDividerMargin;
+    private final int mPaddingTop;
+    private final int mPaddingRight;
+    private final int mPaddingBottom;
+    private final int mPaddingLeft;
+    private final int mGapBetweenImageAndText;
+    private final int mGapBetweenLabelAndData;
+    private final int mCallButtonPadding;
+    private final int mPresenceIconMargin;
+    private final int mHeaderTextWidth;
+
+    private boolean mHorizontalDividerVisible;
+    private Drawable mHorizontalDividerDrawable;
+    private int mHorizontalDividerHeight;
+
+    private boolean mVerticalDividerVisible;
+    private Drawable mVerticalDividerDrawable;
+    private int mVerticalDividerWidth;
+
+    private boolean mHeaderVisible;
+    private Drawable mHeaderBackgroundDrawable;
+    private int mHeaderBackgroundHeight;
+    private TextView mHeaderTextView;
+
+    private QuickContactBadge mQuickContact;
+    private ImageView mPhotoView;
+    private TextView mNameTextView;
+    private DontPressWithParentImageView mCallButton;
+    private TextView mLabelView;
+    private TextView mDataView;
+    private TextView mSnippetView;
+    private ImageView mPresenceIcon;
+
+    private int mPhotoViewWidth;
+    private int mPhotoViewHeight;
+    private int mLine1Height;
+    private int mLine2Height;
+    private int mLine3Height;
+
+    private OnClickListener mCallButtonClickListener;
+
+    public ContactListItemView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mContext = context;
+
+        // Obtain preferred item height from the current theme
+        TypedArray a = context.obtainStyledAttributes(null, com.android.internal.R.styleable.Theme);
+        mPreferredHeight =
+                a.getDimensionPixelSize(android.R.styleable.Theme_listPreferredItemHeight, 0);
+        a.recycle();
+
+        Resources resources = context.getResources();
+        mVerticalDividerMargin =
+                resources.getDimensionPixelOffset(R.dimen.list_item_vertical_divider_margin);
+        mPaddingTop =
+                resources.getDimensionPixelOffset(R.dimen.list_item_padding_top);
+        mPaddingBottom =
+                resources.getDimensionPixelOffset(R.dimen.list_item_padding_bottom);
+        mPaddingLeft =
+                resources.getDimensionPixelOffset(R.dimen.list_item_padding_left);
+        mPaddingRight =
+                resources.getDimensionPixelOffset(R.dimen.list_item_padding_right);
+        mGapBetweenImageAndText =
+                resources.getDimensionPixelOffset(R.dimen.list_item_gap_between_image_and_text);
+        mGapBetweenLabelAndData =
+                resources.getDimensionPixelOffset(R.dimen.list_item_gap_between_label_and_data);
+        mCallButtonPadding =
+                resources.getDimensionPixelOffset(R.dimen.list_item_call_button_padding);
+        mPresenceIconMargin =
+                resources.getDimensionPixelOffset(R.dimen.list_item_presence_icon_margin);
+        mHeaderTextWidth =
+                resources.getDimensionPixelOffset(R.dimen.list_item_header_text_width);
+    }
+
+    /**
+     * Installs a call button listener.
+     */
+    public void setOnCallButtonClickListener(OnClickListener callButtonClickListener) {
+        mCallButtonClickListener = callButtonClickListener;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        // We will match parent's width and wrap content vertically, but make sure
+        // height is no less than listPreferredItemHeight.
+        int width = resolveSize(0, widthMeasureSpec);
+        int height = 0;
+
+        mLine1Height = 0;
+        mLine2Height = 0;
+        mLine3Height = 0;
+
+        // Obtain the natural dimensions of the name text (we only care about height)
+        mNameTextView.measure(0, 0);
+
+        mLine1Height = mNameTextView.getMeasuredHeight();
+
+        if (isVisible(mLabelView)) {
+            mLabelView.measure(0, 0);
+            mLine2Height = mLabelView.getMeasuredHeight();
+        }
+
+        if (isVisible(mDataView)) {
+            mDataView.measure(0, 0);
+            mLine2Height = Math.max(mLine2Height, mDataView.getMeasuredHeight());
+        }
+
+        if (isVisible(mSnippetView)) {
+            mSnippetView.measure(0, 0);
+            mLine3Height = mSnippetView.getMeasuredHeight();
+        }
+
+        height += mLine1Height + mLine2Height + mLine3Height;
+
+        if (isVisible(mCallButton)) {
+            mCallButton.measure(0, 0);
+        }
+        if (isVisible(mPresenceIcon)) {
+            mPresenceIcon.measure(0, 0);
+        }
+
+        ensurePhotoViewSize();
+
+        height = Math.max(height, mPhotoViewHeight);
+        height = Math.max(height, mPreferredHeight);
+
+        if (mHeaderVisible) {
+            ensureHeaderBackground();
+            mHeaderTextView.measure(
+                    MeasureSpec.makeMeasureSpec(mHeaderTextWidth, MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(mHeaderBackgroundHeight, MeasureSpec.EXACTLY));
+            height += mHeaderBackgroundDrawable.getIntrinsicHeight();
+        }
+
+        setMeasuredDimension(width, height);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        int height = bottom - top;
+        int width = right - left;
+
+        // Determine the vertical bounds by laying out the header first.
+        int topBound = 0;
+
+        if (mHeaderVisible) {
+            mHeaderBackgroundDrawable.setBounds(
+                    0,
+                    0,
+                    width,
+                    mHeaderBackgroundHeight);
+            mHeaderTextView.layout(0, 0, width, mHeaderBackgroundHeight);
+            topBound += mHeaderBackgroundHeight;
+        }
+
+        // Positions of views on the left are fixed and so are those on the right side.
+        // The stretchable part of the layout is in the middle.  So, we will start off
+        // by laying out the left and right sides. Then we will allocate the remainder
+        // to the text fields in the middle.
+
+        // Left side
+        int leftBound = mPaddingLeft;
+        View photoView = mQuickContact != null ? mQuickContact : mPhotoView;
+        if (photoView != null) {
+            // Center the photo vertically
+            int photoTop = topBound + (height - topBound - mPhotoViewHeight) / 2;
+            photoView.layout(
+                    leftBound,
+                    photoTop,
+                    leftBound + mPhotoViewWidth,
+                    photoTop + mPhotoViewHeight);
+            leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
+        }
+
+        // Right side
+        int rightBound = right;
+        if (isVisible(mCallButton)) {
+            int buttonWidth = mCallButton.getMeasuredWidth();
+            rightBound -= buttonWidth;
+            mCallButton.layout(
+                    rightBound,
+                    topBound,
+                    rightBound + buttonWidth,
+                    height);
+            mVerticalDividerVisible = true;
+            ensureVerticalDivider();
+            rightBound -= mVerticalDividerWidth;
+            mVerticalDividerDrawable.setBounds(
+                    rightBound,
+                    topBound + mVerticalDividerMargin,
+                    rightBound + mVerticalDividerWidth,
+                    height - mVerticalDividerMargin);
+        } else {
+            mVerticalDividerVisible = false;
+        }
+
+        if (isVisible(mPresenceIcon)) {
+            int iconWidth = mPresenceIcon.getMeasuredWidth();
+            rightBound -= mPresenceIconMargin + iconWidth;
+            mPresenceIcon.layout(
+                    rightBound,
+                    topBound,
+                    rightBound + iconWidth,
+                    height);
+        }
+
+        if (mHorizontalDividerVisible) {
+            ensureHorizontalDivider();
+            mHorizontalDividerDrawable.setBounds(
+                    0,
+                    height - mHorizontalDividerHeight,
+                    width,
+                    height);
+        }
+
+        topBound += mPaddingTop;
+        int bottomBound = height - mPaddingBottom;
+
+        // Text lines, centered vertically
+        rightBound -= mPaddingRight;
+
+        // Center text vertically
+        int totalTextHeight = mLine1Height + mLine2Height + mLine3Height;
+        int textTopBound = (bottomBound + topBound - totalTextHeight) / 2;
+
+        mNameTextView.layout(leftBound,
+                textTopBound,
+                rightBound,
+                textTopBound + mLine1Height);
+
+        int dataLeftBound = leftBound;
+        if (isVisible(mLabelView)) {
+            dataLeftBound = leftBound + mLabelView.getMeasuredWidth();
+            mLabelView.layout(leftBound,
+                    textTopBound + mLine1Height,
+                    dataLeftBound,
+                    textTopBound + mLine1Height + mLine2Height);
+            dataLeftBound += mGapBetweenLabelAndData;
+        }
+
+        if (isVisible(mDataView)) {
+            mDataView.layout(dataLeftBound,
+                    textTopBound + mLine1Height,
+                    rightBound,
+                    textTopBound + mLine1Height + mLine2Height);
+        }
+
+        if (isVisible(mSnippetView)) {
+            mSnippetView.layout(leftBound,
+                    textTopBound + mLine1Height + mLine2Height,
+                    rightBound,
+                    textTopBound + mLine1Height + mLine2Height + mLine3Height);
+        }
+    }
+
+    private boolean isVisible(View view) {
+        return view != null && view.getVisibility() == View.VISIBLE;
+    }
+
+    /**
+     * Loads the drawable for the vertical divider if it has not yet been loaded.
+     */
+    private void ensureVerticalDivider() {
+        if (mVerticalDividerDrawable == null) {
+            mVerticalDividerDrawable = mContext.getResources().getDrawable(
+                    R.drawable.divider_vertical_dark);
+            mVerticalDividerWidth = mVerticalDividerDrawable.getIntrinsicWidth();
+        }
+    }
+
+    /**
+     * Loads the drawable for the horizontal divider if it has not yet been loaded.
+     */
+    private void ensureHorizontalDivider() {
+        if (mHorizontalDividerDrawable == null) {
+            mHorizontalDividerDrawable = mContext.getResources().getDrawable(
+                    com.android.internal.R.drawable.divider_horizontal_dark_opaque);
+            mHorizontalDividerHeight = mHorizontalDividerDrawable.getIntrinsicHeight();
+        }
+    }
+
+    /**
+     * Loads the drawable for the header background if it has not yet been loaded.
+     */
+    private void ensureHeaderBackground() {
+        if (mHeaderBackgroundDrawable == null) {
+            mHeaderBackgroundDrawable = mContext.getResources().getDrawable(
+                    android.R.drawable.dark_header);
+            mHeaderBackgroundHeight = mHeaderBackgroundDrawable.getIntrinsicHeight();
+        }
+    }
+
+    /**
+     * Extracts width and height from the style
+     */
+    private void ensurePhotoViewSize() {
+        if (mPhotoViewWidth == 0 && mPhotoViewHeight == 0) {
+            TypedArray a = mContext.obtainStyledAttributes(null,
+                    com.android.internal.R.styleable.ViewGroup_Layout,
+                    QUICK_CONTACT_BADGE_STYLE, 0);
+            mPhotoViewWidth = a.getLayoutDimension(
+                    android.R.styleable.ViewGroup_Layout_layout_width,
+                    ViewGroup.LayoutParams.WRAP_CONTENT);
+            mPhotoViewHeight = a.getLayoutDimension(
+                    android.R.styleable.ViewGroup_Layout_layout_height,
+                    ViewGroup.LayoutParams.WRAP_CONTENT);
+            a.recycle();
+        }
+    }
+
+    @Override
+    public void dispatchDraw(Canvas canvas) {
+        if (mHeaderVisible) {
+            mHeaderBackgroundDrawable.draw(canvas);
+        }
+        if (mHorizontalDividerVisible) {
+            mHorizontalDividerDrawable.draw(canvas);
+        }
+        if (mVerticalDividerVisible) {
+            mVerticalDividerDrawable.draw(canvas);
+        }
+        super.dispatchDraw(canvas);
+    }
+
+    /**
+     * Sets the flag that determines whether a divider should drawn at the bottom
+     * of the view.
+     */
+    public void setDividerVisible(boolean visible) {
+        mHorizontalDividerVisible = visible;
+    }
+
+    /**
+     * Sets section header or makes it invisible if the title is null.
+     */
+    public void setSectionHeader(String title) {
+        if (!TextUtils.isEmpty(title)) {
+            if (mHeaderTextView == null) {
+                mHeaderTextView = new TextView(mContext);
+                mHeaderTextView.setTypeface(mHeaderTextView.getTypeface(), Typeface.BOLD);
+                mHeaderTextView.setTextColor(mContext.getResources()
+                        .getColor(com.android.internal.R.color.dim_foreground_dark));
+                mHeaderTextView.setTextSize(14);
+                mHeaderTextView.setGravity(Gravity.CENTER);
+                addView(mHeaderTextView);
+            }
+            mHeaderTextView.setText(title);
+            mHeaderTextView.setVisibility(View.VISIBLE);
+            mHeaderVisible = true;
+        } else {
+            if (mHeaderTextView != null) {
+                mHeaderTextView.setVisibility(View.GONE);
+            }
+            mHeaderVisible = false;
+        }
+    }
+
+    /**
+     * Returns the quick contact badge, creating it if necessary.
+     */
+    public QuickContactBadge getQuickContact() {
+        if (mQuickContact == null) {
+            mQuickContact = new QuickContactBadge(mContext, null, QUICK_CONTACT_BADGE_STYLE);
+            mQuickContact.setExcludeMimes(new String[] { Contacts.CONTENT_ITEM_TYPE });
+            addView(mQuickContact);
+        }
+        return mQuickContact;
+    }
+
+    /**
+     * Returns the photo view, creating it if necessary.
+     */
+    public ImageView getPhotoView() {
+        if (mPhotoView == null) {
+            mPhotoView = new ImageView(mContext, null, QUICK_CONTACT_BADGE_STYLE);
+            // Quick contact style used above will set a background - remove it
+            mPhotoView.setBackgroundDrawable(null);
+            addView(mPhotoView);
+        }
+        return mPhotoView;
+    }
+
+    /**
+     * Returns the text view for the contact name, creating it if necessary.
+     */
+    public TextView getNameTextView() {
+        if (mNameTextView == null) {
+            mNameTextView = new TextView(mContext);
+            mNameTextView.setSingleLine(true);
+            mNameTextView.setEllipsize(TruncateAt.MARQUEE);
+            mNameTextView.setTextAppearance(mContext, android.R.style.TextAppearance_Large);
+            mNameTextView.setGravity(Gravity.CENTER_VERTICAL);
+            addView(mNameTextView);
+        }
+        return mNameTextView;
+    }
+
+    /**
+     * Adds a call button using the supplied arguments as an id and tag.
+     */
+    public void showCallButton(int id, int tag) {
+        if (mCallButton == null) {
+            mCallButton = new DontPressWithParentImageView(mContext, null);
+            mCallButton.setId(id);
+            mCallButton.setOnClickListener(mCallButtonClickListener);
+            mCallButton.setBackgroundResource(R.drawable.call_background);
+            mCallButton.setImageResource(android.R.drawable.sym_action_call);
+            mCallButton.setPadding(mCallButtonPadding, 0, mCallButtonPadding, 0);
+            mCallButton.setScaleType(ScaleType.CENTER);
+            addView(mCallButton);
+        }
+
+        mCallButton.setTag(tag);
+        mCallButton.setVisibility(View.VISIBLE);
+    }
+
+    public void hideCallButton() {
+        if (mCallButton != null) {
+            mCallButton.setVisibility(View.GONE);
+        }
+    }
+
+    /**
+     * Adds or updates a text view for the data label.
+     */
+    public void setLabel(CharSequence text) {
+        if (TextUtils.isEmpty(text)) {
+            if (mLabelView != null) {
+                mLabelView.setVisibility(View.GONE);
+            }
+        } else {
+            getLabelView().setText(text);
+        }
+    }
+
+    /**
+     * Adds or updates a text view for the data label.
+     */
+    public void setLabel(char[] text, int size) {
+        if (text == null || size == 0) {
+            if (mLabelView != null) {
+                mLabelView.setVisibility(View.GONE);
+            }
+        } else {
+            getLabelView().setText(text, 0, size);
+        }
+    }
+
+    /**
+     * Returns the text view for the data label, creating it if necessary.
+     */
+    public TextView getLabelView() {
+        if (mLabelView == null) {
+            mLabelView = new TextView(mContext);
+            mLabelView.setSingleLine(true);
+            mLabelView.setEllipsize(TruncateAt.MARQUEE);
+            mLabelView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
+            mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD);
+            addView(mLabelView);
+        }
+        return mLabelView;
+    }
+
+    /**
+     * Adds or updates a text view for the data element.
+     */
+    public void setData(char[] text, int size) {
+        if (text == null || size == 0) {
+            if (mDataView != null) {
+                mDataView.setVisibility(View.GONE);
+            }
+            return;
+        } else {
+            getDataView().setText(text, 0, size);
+        }
+    }
+
+    /**
+     * Returns the text view for the data text, creating it if necessary.
+     */
+    public TextView getDataView() {
+        if (mDataView == null) {
+            mDataView = new TextView(mContext);
+            mDataView.setSingleLine(true);
+            mDataView.setEllipsize(TruncateAt.MARQUEE);
+            mDataView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
+            addView(mDataView);
+        }
+        return mDataView;
+    }
+
+    /**
+     * Adds or updates a text view for the search snippet.
+     */
+    public void setSnippet(CharSequence text) {
+        if (TextUtils.isEmpty(text)) {
+            if (mSnippetView != null) {
+                mSnippetView.setVisibility(View.GONE);
+            }
+        } else {
+            getSnippetView().setText(text);
+        }
+    }
+
+    /**
+     * Returns the text view for the search snippet, creating it if necessary.
+     */
+    public TextView getSnippetView() {
+        if (mSnippetView == null) {
+            mSnippetView = new TextView(mContext);
+            mSnippetView.setSingleLine(true);
+            mSnippetView.setEllipsize(TruncateAt.MARQUEE);
+            mSnippetView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
+            mSnippetView.setTypeface(mSnippetView.getTypeface(), Typeface.BOLD);
+            addView(mSnippetView);
+        }
+        return mSnippetView;
+    }
+
+    /**
+     * Adds or updates the presence icon view.
+     */
+    public void setPresence(Drawable icon) {
+        if (icon != null) {
+            if (mPresenceIcon == null) {
+                mPresenceIcon = new ImageView(mContext);
+                addView(mPresenceIcon);
+            }
+            mPresenceIcon.setImageDrawable(icon);
+            mPresenceIcon.setScaleType(ScaleType.CENTER);
+            mPresenceIcon.setVisibility(View.VISIBLE);
+        } else {
+            if (mPresenceIcon != null) {
+                mPresenceIcon.setVisibility(View.GONE);
+            }
+        }
+    }
+}
diff --git a/src/com/android/contacts/ContactsListActivity.java b/src/com/android/contacts/ContactsListActivity.java
index da96e2b..6fd03f7 100644
--- a/src/com/android/contacts/ContactsListActivity.java
+++ b/src/com/android/contacts/ContactsListActivity.java
@@ -112,11 +112,11 @@
 import android.widget.AdapterView;
 import android.widget.ArrayAdapter;
 import android.widget.Button;
+import android.widget.CursorAdapter;
 import android.widget.Filter;
 import android.widget.ImageView;
 import android.widget.ListView;
 import android.widget.QuickContactBadge;
-import android.widget.ResourceCursorAdapter;
 import android.widget.SectionIndexer;
 import android.widget.TextView;
 import android.widget.Toast;
@@ -511,10 +511,10 @@
         protected void invalidate() {
             int childCount = mListView.getChildCount();
             for (int i = 0; i < childCount; i++) {
-                View listItem = mListView.getChildAt(i);
-                Object tag = listItem.getTag();
-                if (tag instanceof ContactListItemCache) {
-                    ((ContactListItemCache)tag).nameView.invalidate();
+                View itemView = mListView.getChildAt(i);
+                if (itemView instanceof ContactListItemView) {
+                    final ContactListItemView view = (ContactListItemView)itemView;
+                    view.getNameTextView().invalidate();
                 }
             }
         }
@@ -929,7 +929,8 @@
     public void onClick(View v) {
         int id = v.getId();
         switch (id) {
-            case R.id.call_button: {
+            // TODO a better way of identifying the button
+            case android.R.id.button1: {
                 final int position = (Integer)v.getTag();
                 Cursor c = mAdapter.getCursor();
                 if (c != null) {
@@ -2748,20 +2749,8 @@
     }
 
     final static class ContactListItemCache {
-        public View header;
-        public TextView headerText;
-        public View divider;
-        public TextView nameView;
-        public View callView;
-        public ImageView callButton;
         public CharArrayBuffer nameBuffer = new CharArrayBuffer(128);
-        public TextView labelView;
-        public TextView dataView;
-        public TextView snippetView;
         public CharArrayBuffer dataBuffer = new CharArrayBuffer(128);
-        public ImageView presenceView;
-        public QuickContactBadge photoView;
-        public ImageView nonQuickContactPhotoView;
         public CharArrayBuffer highlightedTextBuffer = new CharArrayBuffer(128);
         public TextWithHighlighting textWithHighlighting;
         public CharArrayBuffer phoneticNameBuffer = new CharArrayBuffer(128);
@@ -2773,7 +2762,7 @@
         public Drawable background;
     }
 
-    private final class ContactItemListAdapter extends ResourceCursorAdapter
+    private final class ContactItemListAdapter extends CursorAdapter
             implements SectionIndexer, OnScrollListener, PinnedHeaderListView.PinnedHeaderAdapter {
         private SectionIndexer mIndexer;
         private boolean mLoading = true;
@@ -2787,7 +2776,7 @@
         private int mSuggestionsCursorCount;
 
         public ContactItemListAdapter(Context context) {
-            super(context, R.layout.contacts_list_item, null, false);
+            super(context, null, false);
 
             mUnknownNameText = context.getText(android.R.string.unknownName);
             switch (mMode) {
@@ -2823,11 +2812,6 @@
 
             if ((mMode & MODE_MASK_SHOW_PHOTOS) == MODE_MASK_SHOW_PHOTOS) {
                 mDisplayPhotos = true;
-                if (mShowSearchSnippets) {
-                    setViewResource(R.layout.contacts_list_item_photo_and_snippet);
-                } else {
-                    setViewResource(R.layout.contacts_list_item_photo);
-                }
             }
         }
 
@@ -2960,10 +2944,13 @@
                 throw new IllegalStateException("couldn't move cursor to position " + position);
             }
 
+            boolean newView;
             View v;
             if (convertView == null || convertView.getTag() == null) {
+                newView = true;
                 v = newView(mContext, cursor, parent);
             } else {
+                newView = false;
                 v = convertView;
             }
             bindView(v, mContext, cursor);
@@ -2971,7 +2958,6 @@
             return v;
         }
 
-
         private View getTotalContactCountView(ViewGroup parent) {
             final LayoutInflater inflater = getLayoutInflater();
             View view = inflater.inflate(R.layout.total_contacts, parent, false);
@@ -3023,39 +3009,17 @@
 
         @Override
         public View newView(Context context, Cursor cursor, ViewGroup parent) {
-            final View view = super.newView(context, cursor, parent);
-
-            final ContactListItemCache cache = new ContactListItemCache();
-            cache.header = view.findViewById(R.id.header);
-            cache.headerText = (TextView)view.findViewById(R.id.header_text);
-            cache.divider = view.findViewById(R.id.list_divider);
-            cache.nameView = (TextView) view.findViewById(R.id.name);
-            cache.callView = view.findViewById(R.id.call_view);
-            cache.callButton = (ImageView) view.findViewById(R.id.call_button);
-            if (cache.callButton != null) {
-                cache.callButton.setOnClickListener(ContactsListActivity.this);
-            }
-            cache.labelView = (TextView) view.findViewById(R.id.label);
-            cache.dataView = (TextView) view.findViewById(R.id.data);
-            cache.presenceView = (ImageView) view.findViewById(R.id.presence);
-            cache.photoView = (QuickContactBadge) view.findViewById(R.id.photo);
-            if (cache.photoView != null) {
-                cache.photoView.setExcludeMimes(new String[] {Contacts.CONTENT_ITEM_TYPE});
-            }
-            cache.nonQuickContactPhotoView = (ImageView) view.findViewById(R.id.noQuickContactPhoto);
-            cache.textWithHighlighting = mHighlightingAnimation.createTextWithHighlighting();
-            cache.snippetView = (TextView)view.findViewById(R.id.snippet);
-
-            view.setTag(cache);
+            final ContactListItemView view = new ContactListItemView(context, null);
+            view.setOnCallButtonClickListener(ContactsListActivity.this);
+            view.setTag(new ContactListItemCache());
             return view;
         }
 
         @Override
-        public void bindView(View view, Context context, Cursor cursor) {
+        public void bindView(View itemView, Context context, Cursor cursor) {
+            final ContactListItemView view = (ContactListItemView)itemView;
             final ContactListItemCache cache = (ContactListItemCache) view.getTag();
 
-            TextView dataView = cache.dataView;
-            TextView labelView = cache.labelView;
             int typeColumnIndex;
             int dataColumnIndex;
             int labelColumnIndex;
@@ -3100,16 +3064,21 @@
 
             // Set the name
             cursor.copyStringToBuffer(nameColumnIndex, cache.nameBuffer);
+            TextView nameView = view.getNameTextView();
             int size = cache.nameBuffer.sizeCopied;
             if (size != 0) {
                 if (highlightingEnabled) {
-                    buildDisplayNameWithHighlighting(cache.nameView, cursor, cache.nameBuffer,
+                    if (cache.textWithHighlighting == null) {
+                        cache.textWithHighlighting =
+                                mHighlightingAnimation.createTextWithHighlighting();
+                    }
+                    buildDisplayNameWithHighlighting(nameView, cursor, cache.nameBuffer,
                             cache.highlightedTextBuffer, cache.textWithHighlighting);
                 } else {
-                    cache.nameView.setText(cache.nameBuffer.data, 0, size);
+                    nameView.setText(cache.nameBuffer.data, 0, size);
                 }
             } else {
-                cache.nameView.setText(mUnknownNameText);
+                nameView.setText(mUnknownNameText);
             }
 
             boolean hasPhone = cursor.getColumnCount() >= SUMMARY_HAS_PHONE_COLUMN_INDEX
@@ -3118,10 +3087,9 @@
             // Make the call button visible if requested.
             if (mDisplayCallButton && hasPhone) {
                 int pos = cursor.getPosition();
-                cache.callView.setVisibility(View.VISIBLE);
-                cache.callButton.setTag(pos);
+                view.showCallButton(android.R.id.button1, pos);
             } else {
-                cache.callView.setVisibility(View.GONE);
+                view.hideCallButton();
             }
 
             // Set the photo, if requested
@@ -3135,25 +3103,20 @@
 
                 ImageView viewToUse;
                 if (useQuickContact) {
-                    viewToUse = cache.photoView;
                     // Build soft lookup reference
                     final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
                     final String lookupKey = cursor.getString(SUMMARY_LOOKUP_KEY_COLUMN_INDEX);
-                    cache.photoView.assignContactUri(Contacts.getLookupUri(contactId, lookupKey));
-                    cache.photoView.setVisibility(View.VISIBLE);
-                    cache.nonQuickContactPhotoView.setVisibility(View.INVISIBLE);
+                    QuickContactBadge quickContact = view.getQuickContact();
+                    quickContact.assignContactUri(Contacts.getLookupUri(contactId, lookupKey));
+                    viewToUse = quickContact;
                 } else {
-                    viewToUse = cache.nonQuickContactPhotoView;
-                    cache.photoView.setVisibility(View.INVISIBLE);
-                    cache.nonQuickContactPhotoView.setVisibility(View.VISIBLE);
+                    viewToUse = view.getPhotoView();
                 }
 
-
                 final int position = cursor.getPosition();
                 mPhotoLoader.loadPhoto(viewToUse, photoId);
             }
 
-            ImageView presenceView = cache.presenceView;
             if ((mMode & MODE_MASK_NO_PRESENCE) == 0) {
                 // Set the proper icon (star or presence or nothing)
                 int serverStatus;
@@ -3161,27 +3124,24 @@
                     serverStatus = cursor.getInt(SUMMARY_PRESENCE_STATUS_COLUMN_INDEX);
                     Drawable icon = ContactPresenceIconUtil.getPresenceIcon(mContext, serverStatus);
                     if (icon != null) {
-                        presenceView.setImageDrawable(icon);
-                        presenceView.setVisibility(View.VISIBLE);
+                        view.setPresence(icon);
                     } else {
-                        presenceView.setVisibility(View.GONE);
+                        view.setPresence(null);
                     }
                 } else {
-                    presenceView.setVisibility(View.GONE);
+                    view.setPresence(null);
                 }
             } else {
-                presenceView.setVisibility(View.GONE);
+                view.setPresence(null);
             }
 
-            // TODO: make sure that when mShowSearchSnippets is true, the
-            // snippet views are available
-            if (mShowSearchSnippets && cache.snippetView != null) {
+            if (mShowSearchSnippets) {
                 boolean showSnippet = false;
                 String snippetMimeType = cursor.getString(SUMMARY_SNIPPET_MIMETYPE_COLUMN_INDEX);
                 if (Email.CONTENT_ITEM_TYPE.equals(snippetMimeType)) {
                     String email = cursor.getString(SUMMARY_SNIPPET_DATA1_COLUMN_INDEX);
                     if (!TextUtils.isEmpty(email)) {
-                        cache.snippetView.setText(email);
+                        view.setSnippet(email);
                         showSnippet = true;
                     }
                 } else if (Organization.CONTENT_ITEM_TYPE.equals(snippetMimeType)) {
@@ -3189,42 +3149,41 @@
                     String title = cursor.getString(SUMMARY_SNIPPET_DATA4_COLUMN_INDEX);
                     if (!TextUtils.isEmpty(company)) {
                         if (!TextUtils.isEmpty(title)) {
-                            cache.snippetView.setText(company + " / " + title);
+                            view.setSnippet(company + " / " + title);
                         } else {
-                            cache.snippetView.setText(company);
+                            view.setSnippet(company);
                         }
                         showSnippet = true;
                     } else if (!TextUtils.isEmpty(title)) {
-                        cache.snippetView.setText(title);
+                        view.setSnippet(title);
                         showSnippet = true;
                     }
                 } else if (Nickname.CONTENT_ITEM_TYPE.equals(snippetMimeType)) {
                     String nickname = cursor.getString(SUMMARY_SNIPPET_DATA1_COLUMN_INDEX);
                     if (!TextUtils.isEmpty(nickname)) {
-                        cache.snippetView.setText(nickname);
+                        view.setSnippet(nickname);
                         showSnippet = true;
                     }
                 }
 
-                cache.snippetView.setVisibility(showSnippet ? View.VISIBLE : View.GONE);
+                if (!showSnippet) {
+                    view.setSnippet(null);
+                }
             }
 
             if (!displayAdditionalData) {
-                cache.dataView.setVisibility(View.GONE);
-
                 if (phoneticNameColumnIndex != -1) {
 
                     // Set the name
                     cursor.copyStringToBuffer(phoneticNameColumnIndex, cache.phoneticNameBuffer);
                     int phoneticNameSize = cache.phoneticNameBuffer.sizeCopied;
                     if (phoneticNameSize != 0) {
-                        cache.labelView.setText(cache.phoneticNameBuffer.data, 0, phoneticNameSize);
-                        cache.labelView.setVisibility(View.VISIBLE);
+                        view.setLabel(cache.phoneticNameBuffer.data, phoneticNameSize);
                     } else {
-                        cache.labelView.setVisibility(View.GONE);
+                        view.setLabel(null);
                     }
                 } else {
-                    cache.labelView.setVisibility(View.GONE);
+                    view.setLabel(null);
                 }
                 return;
             }
@@ -3233,29 +3192,23 @@
             cursor.copyStringToBuffer(dataColumnIndex, cache.dataBuffer);
 
             size = cache.dataBuffer.sizeCopied;
-            if (size != 0) {
-                dataView.setText(cache.dataBuffer.data, 0, size);
-                dataView.setVisibility(View.VISIBLE);
-            } else {
-                dataView.setVisibility(View.GONE);
-            }
+            view.setData(cache.dataBuffer.data, size);
 
             // Set the label.
             if (!cursor.isNull(typeColumnIndex)) {
-                labelView.setVisibility(View.VISIBLE);
-
                 final int type = cursor.getInt(typeColumnIndex);
                 final String label = cursor.getString(labelColumnIndex);
 
                 if (mMode == MODE_LEGACY_PICK_POSTAL || mMode == MODE_PICK_POSTAL) {
-                    labelView.setText(StructuredPostal.getTypeLabel(context.getResources(), type,
+                    // TODO cache
+                    view.setLabel(StructuredPostal.getTypeLabel(context.getResources(), type,
                             label));
                 } else {
-                    labelView.setText(Phone.getTypeLabel(context.getResources(), type, label));
+                    // TODO cache
+                    view.setLabel(Phone.getTypeLabel(context.getResources(), type, label));
                 }
             } else {
-                // There is no label, hide the the view
-                labelView.setVisibility(View.GONE);
+                view.setLabel(null);
             }
         }
 
@@ -3278,30 +3231,27 @@
             textView.setText(textWithHighlighting);
         }
 
-        private void bindSectionHeader(View view, int position, boolean displaySectionHeaders) {
+        private void bindSectionHeader(View itemView, int position, boolean displaySectionHeaders) {
+            final ContactListItemView view = (ContactListItemView)itemView;
             final ContactListItemCache cache = (ContactListItemCache) view.getTag();
             if (!displaySectionHeaders) {
-                cache.header.setVisibility(View.GONE);
-                cache.divider.setVisibility(View.VISIBLE);
+                view.setSectionHeader(null);
+                view.setDividerVisible(true);
             } else {
                 final int section = getSectionForPosition(position);
                 if (getPositionForSection(section) == position) {
                     String title = (String)mIndexer.getSections()[section];
-                    if (!TextUtils.isEmpty(title)) {
-                        cache.headerText.setText(title);
-                        cache.header.setVisibility(View.VISIBLE);
-                    } else {
-                        cache.header.setVisibility(View.GONE);
-                    }
+                    view.setSectionHeader(title);
                 } else {
-                    cache.header.setVisibility(View.GONE);
+                    view.setDividerVisible(false);
+                    view.setSectionHeader(null);
                 }
 
                 // move the divider for the last item in a section
                 if (getPositionForSection(section + 1) - 1 == position) {
-                    cache.divider.setVisibility(View.GONE);
+                    view.setDividerVisible(false);
                 } else {
-                    cache.divider.setVisibility(View.VISIBLE);
+                    view.setDividerVisible(true);
                 }
             }
         }
@@ -3337,8 +3287,6 @@
                 foundContactsText.setText(text);
             }
 
-            mPhotoLoader.clear();
-
             super.changeCursor(cursor);
             // Update the indexer for the fast scroll widget
             updateIndexer(cursor);
diff --git a/src/com/android/contacts/ImportVCardActivity.java b/src/com/android/contacts/ImportVCardActivity.java
index a8b3f1e..4dcf5d5 100644
--- a/src/com/android/contacts/ImportVCardActivity.java
+++ b/src/com/android/contacts/ImportVCardActivity.java
@@ -31,6 +31,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Environment;
+import android.os.FileUtils;
 import android.os.Handler;
 import android.os.PowerManager;
 import android.pim.vcard.VCardConfig;
@@ -53,9 +54,12 @@
 import android.text.style.RelativeSizeSpan;
 import android.util.Log;
 
+import java.io.BufferedOutputStream;
 import java.io.File;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
@@ -153,6 +157,7 @@
         private boolean mCanceled;
         private PowerManager.WakeLock mWakeLock;
         private Uri mUri;
+        private File mTempFile;
 
         private List<VCardFile> mSelectedVCardFileList;
         private List<String> mErrorFileNameList;
@@ -190,11 +195,18 @@
             boolean shouldCallFinish = true;
             mWakeLock.acquire();
             Uri createdUri = null;
+            mTempFile = null;
             // Some malicious vCard data may make this thread broken
             // (e.g. OutOfMemoryError).
             // Even in such cases, some should be done.
             try {
                 if (mUri != null) {  // Read one vCard expressed by mUri
+                    final Uri uri = getReopenableUri(mUri);
+                    if (uri == null) {
+                        shouldCallFinish = false;
+                        return;
+                    }
+
                     mProgressDialogForReadVCard.setProgressNumberFormat("");
                     mProgressDialogForReadVCard.setProgress(0);
 
@@ -208,16 +220,15 @@
                     VCardSourceDetector detector = new VCardSourceDetector();
                     VCardInterpreterCollection builderCollection = new VCardInterpreterCollection(
                             Arrays.asList(counter, detector));
-
                     boolean result;
                     try {
-                        result = readOneVCardFile(mUri,
+                        result = readOneVCardFile(uri,
                                 VCardConfig.DEFAULT_CHARSET, builderCollection, null, true, null);
                     } catch (VCardNestedException e) {
                         try {
                             // Assume that VCardSourceDetector was able to detect the source.
                             // Try again with the detector.
-                            result = readOneVCardFile(mUri,
+                            result = readOneVCardFile(uri,
                                     VCardConfig.DEFAULT_CHARSET, counter, detector, false, null);
                         } catch (VCardNestedException e2) {
                             result = false;
@@ -239,7 +250,7 @@
                     mProgressDialogForReadVCard.setIndeterminate(false);
                     mProgressDialogForReadVCard.setMax(counter.getCount());
                     String charset = detector.getEstimatedCharset();
-                    createdUri = doActuallyReadOneVCard(mUri, null, charset, true, detector,
+                    createdUri = doActuallyReadOneVCard(uri, null, charset, true, detector,
                             mErrorFileNameList);
                 } else {  // Read multiple files.
                     mProgressDialogForReadVCard.setProgressNumberFormat(
@@ -251,7 +262,13 @@
                         if (mCanceled) {
                             return;
                         }
-                        final Uri uri = Uri.parse("file://" + vcardFile.getCanonicalPath());
+                        // TODO: detect scheme!
+                        final Uri uri = getReopenableUri(
+                                Uri.parse("file://" + vcardFile.getCanonicalPath()));
+                        if (uri == null) {
+                            shouldCallFinish = false;
+                            return;
+                        }
 
                         VCardSourceDetector detector = new VCardSourceDetector();
                         try {
@@ -271,6 +288,12 @@
             } finally {
                 mWakeLock.release();
                 mProgressDialogForReadVCard.dismiss();
+                if (mTempFile != null) {
+                    if (!mTempFile.delete()) {
+                        Log.w(LOG_TAG, "Failed to delete a cache file.");
+                    }
+                    mTempFile = null;
+                }
                 // finish() is called via mCancelListener, which is used in DialogDisplayer.
                 if (shouldCallFinish && !isFinishing()) {
                     if (mErrorFileNameList == null || mErrorFileNameList.isEmpty()) {
@@ -310,6 +333,49 @@
             }
         }
 
+        private Uri getReopenableUri(final Uri uri) {
+            if ("file".equals(uri.getScheme())) {
+                return uri;
+            } else {
+                // We may not be able to scan a given uri more than once when it does not
+                // point to a local file, while it is necessary to scan it more than once
+                // because of vCard limitation. We rely on a local temporary file instead.
+                //
+                // e.g. Email app's AttachmentProvider is able to give us a content Uri
+                // with an attachment file, but we cannot "sometimes" (not always) scan
+                // the Uri more than once because of permission revocation.
+                InputStream is = null;
+                OutputStream os = null;
+                Uri reopenableUri = null;
+                try {
+                    is = mResolver.openInputStream(uri);
+                    File dir = getDir("tmp", MODE_PRIVATE);
+                    mTempFile = dir.createTempFile("vcf", null, dir);
+                    FileUtils.copyToFile(is, mTempFile);
+                    reopenableUri = Uri.parse("file://" + mTempFile.getCanonicalPath());
+                } catch (IOException e) {
+                    mHandler.post(new DialogDisplayer(
+                            getString(R.string.fail_reason_io_error) +
+                            ": " + e.getLocalizedMessage()));
+                    return null;
+                } finally {
+                    if (is != null) {
+                        try {
+                            is.close();
+                        } catch (IOException e) {
+                        }
+                    }
+                    if (os != null) {
+                        try {
+                            os.close();
+                        } catch (IOException e) {
+                        }
+                    }
+                }
+                return reopenableUri;
+            }
+        }
+
         private Uri doActuallyReadOneVCard(Uri uri, Account account,
                 String charset, boolean showEntryParseProgress,
                 VCardSourceDetector detector, List<String> errorFileNameList) {
diff --git a/src/com/android/contacts/ViewContactActivity.java b/src/com/android/contacts/ViewContactActivity.java
index ec365dc..ead6a4a 100644
--- a/src/com/android/contacts/ViewContactActivity.java
+++ b/src/com/android/contacts/ViewContactActivity.java
@@ -15,7 +15,6 @@
  */
 
 package com.android.contacts;
-
 import com.android.contacts.Collapser.Collapsible;
 import com.android.contacts.model.ContactsSource;
 import com.android.contacts.model.Sources;
@@ -92,6 +91,7 @@
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 
 /**
  * Displays the details of a specific contact.
@@ -333,16 +333,13 @@
         (new AsyncTask<Void, Void, ArrayList<Entity>>() {
             @Override
             protected ArrayList<Entity> doInBackground(Void... params) {
-                ArrayList<Entity> newEntities = Lists.newArrayList();
+                ArrayList<Entity> newEntities = new ArrayList<Entity>(cursor.getCount());
                 EntityIterator iterator = RawContacts.newEntityIterator(cursor);
                 try {
                     while (iterator.hasNext()) {
                         Entity entity = iterator.next();
                         newEntities.add(entity);
                     }
-                } catch (RemoteException e) {
-                    Log.w(TAG, "Problem reading contact data: " + e.toString());
-                    return null;
                 } finally {
                     iterator.close();
                 }
@@ -394,19 +391,65 @@
         mHasStatuses = true;
     }
 
+    private static Cursor setupContactCursor(ContentResolver resolver, Uri lookupUri) {
+        if (lookupUri == null) {
+            return null;
+        }
+        final List<String> segments = lookupUri.getPathSegments();
+        if (segments.size() != 4) {
+            return null;
+        }
+
+        // Contains an Id.
+        final long uriContactId = Long.parseLong(segments.get(3));
+        final String uriLookupKey = Uri.encode(segments.get(2));
+        final Uri dataUri = Uri.withAppendedPath(
+                ContentUris.withAppendedId(Contacts.CONTENT_URI, uriContactId),
+                Contacts.Data.CONTENT_DIRECTORY);
+
+        // This cursor has several purposes:
+        // - Fetch NAME_RAW_CONTACT_ID and DISPLAY_NAME_SOURCE
+        // - Fetch the lookup-key to ensure we are looking at the right record
+        // - Watcher for change events
+        Cursor cursor = resolver.query(dataUri,
+                new String[] {
+                    Contacts.NAME_RAW_CONTACT_ID,
+                    Contacts.DISPLAY_NAME_SOURCE,
+                    Contacts.LOOKUP_KEY
+                }, null, null, null);
+
+        if (cursor.moveToFirst()) {
+            String lookupKey =
+                    cursor.getString(cursor.getColumnIndex(Contacts.LOOKUP_KEY));
+            if (!lookupKey.equals(uriLookupKey)) {
+                // ID and lookup key do not match
+                cursor.close();
+                return null;
+            }
+            return cursor;
+        } else {
+            cursor.close();
+            return null;
+        }
+    }
+
     private synchronized void startEntityQuery() {
         closeCursor();
 
-        Uri uri = null;
-        if (mLookupUri != null) {
+        // Interprete mLookupUri
+        mCursor = setupContactCursor(mResolver, mLookupUri);
+
+        // If mCursor is null now we did not succeed in using the Uri's Id (or it didn't contain
+        // a Uri). Instead we now have to use the lookup key to find the record
+        if (mCursor == null) {
             mLookupUri = Contacts.getLookupUri(getContentResolver(), mLookupUri);
-            if (mLookupUri != null) {
-                uri = Contacts.lookupContact(getContentResolver(), mLookupUri);
-            }
+            mCursor = setupContactCursor(mResolver, mLookupUri);
         }
 
-        if (uri == null) {
-
+        // If mCursor is still null, we were unsuccessful in finding the record
+        if (mCursor == null) {
+            mNameRawContactId = -1;
+            mDisplayNameSource = DisplayNameSources.UNDEFINED;
             // TODO either figure out a way to prevent a flash of black background or
             // use some other UI than a toast
             Toast.makeText(this, R.string.invalidContactMessage, Toast.LENGTH_SHORT).show();
@@ -415,34 +458,26 @@
             return;
         }
 
-        final Uri dataUri = Uri.withAppendedPath(uri, Contacts.Data.CONTENT_DIRECTORY);
+        final long contactId = ContentUris.parseId(mLookupUri);
 
-        // This cursor has two purposes:
-        // - Fetch NAME_RAW_CONTACT_ID and DISPLAY_NAME_SOURCE
-        // - Watcher for change events
-        mCursor = mResolver.query(dataUri,
-                new String[] { Contacts.NAME_RAW_CONTACT_ID, Contacts.DISPLAY_NAME_SOURCE },
-                null, null, null);
-
-        if (mCursor.moveToFirst()) {
-            mNameRawContactId =
+        mNameRawContactId =
                 mCursor.getLong(mCursor.getColumnIndex(Contacts.NAME_RAW_CONTACT_ID));
-            mDisplayNameSource =
+        mDisplayNameSource =
                 mCursor.getInt(mCursor.getColumnIndex(Contacts.DISPLAY_NAME_SOURCE));
-        } else {
-            mNameRawContactId = -1;
-            mDisplayNameSource = DisplayNameSources.UNDEFINED;
-        }
 
         mCursor.registerContentObserver(mObserver);
-        final long contactId = ContentUris.parseId(uri);
 
         // Clear flags and start queries to data and status
         mHasEntities = false;
         mHasStatuses = false;
 
         mHandler.startQuery(TOKEN_ENTITIES, null, RawContactsEntity.CONTENT_URI, null,
-                RawContacts.CONTACT_ID + "=" + contactId, null, null);
+                RawContacts.CONTACT_ID + "=?", new String[] {
+                    String.valueOf(contactId)
+                }, null);
+        final Uri dataUri = Uri.withAppendedPath(
+                ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId),
+                Contacts.Data.CONTENT_DIRECTORY);
         mHandler.startQuery(TOKEN_STATUSES, null, dataUri, StatusQuery.PROJECTION,
                         StatusUpdates.PRESENCE + " IS NOT NULL OR " + StatusUpdates.STATUS
                                 + " IS NOT NULL", null, null);
diff --git a/src/com/android/contacts/model/EntitySet.java b/src/com/android/contacts/model/EntitySet.java
index 7502d0f..83fe338 100644
--- a/src/com/android/contacts/model/EntitySet.java
+++ b/src/com/android/contacts/model/EntitySet.java
@@ -23,7 +23,6 @@
 import android.content.ContentProviderOperation.Builder;
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.os.RemoteException;
 import android.provider.ContactsContract.AggregationExceptions;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.RawContacts;
@@ -76,8 +75,6 @@
                 state.add(entity);
             }
             return state;
-        } catch (RemoteException e) {
-            throw new IllegalStateException("Problem querying contact details", e);
         } finally {
             iterator.close();
         }
diff --git a/src/com/android/contacts/ui/ContactsPreferencesActivity.java b/src/com/android/contacts/ui/ContactsPreferencesActivity.java
index b6a20c7..0432aaf 100644
--- a/src/com/android/contacts/ui/ContactsPreferencesActivity.java
+++ b/src/com/android/contacts/ui/ContactsPreferencesActivity.java
@@ -574,8 +574,6 @@
                 // Create single entry handling ungrouped status
                 mUngrouped = GroupDelta.fromSettings(resolver, accountName, accountType, hasGroups);
                 addGroup(mUngrouped);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Problem reading groups: " + e.toString());
             } finally {
                 iterator.close();
             }
diff --git a/src/com/android/contacts/ui/EditContactActivity.java b/src/com/android/contacts/ui/EditContactActivity.java
index 25132fb..c70cff6 100644
--- a/src/com/android/contacts/ui/EditContactActivity.java
+++ b/src/com/android/contacts/ui/EditContactActivity.java
@@ -67,7 +67,6 @@
 import android.provider.ContactsContract.RawContacts;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.Contacts.Data;
 import android.util.Log;
 import android.view.ContextThemeWrapper;
@@ -111,6 +110,9 @@
     private static final String KEY_EDIT_STATE = "state";
     private static final String KEY_RAW_CONTACT_ID_REQUESTING_PHOTO = "photorequester";
     private static final String KEY_VIEW_ID_GENERATOR = "viewidgenerator";
+    private static final String KEY_CURRENT_PHOTO_FILE = "currentphotofile";
+    private static final String KEY_QUERY_SELECTION = "queryselection";
+    private static final String KEY_CONTACT_ID_FOR_JOIN = "contactidforjoin";
 
     /** The result code when view activity should close after edit returns */
     public static final int RESULT_CLOSE_VIEW_ACTIVITY = 777;
@@ -265,6 +267,11 @@
 
         outState.putLong(KEY_RAW_CONTACT_ID_REQUESTING_PHOTO, mRawContactIdRequestingPhoto);
         outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator);
+        if (mCurrentPhotoFile != null) {
+            outState.putString(KEY_CURRENT_PHOTO_FILE, mCurrentPhotoFile.toString());
+        }
+        outState.putString(KEY_QUERY_SELECTION, mQuerySelection);
+        outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin);
         super.onSaveInstanceState(outState);
     }
 
@@ -275,6 +282,13 @@
         mRawContactIdRequestingPhoto = savedInstanceState.getLong(
                 KEY_RAW_CONTACT_ID_REQUESTING_PHOTO);
         mViewIdGenerator = savedInstanceState.getParcelable(KEY_VIEW_ID_GENERATOR);
+        String fileName = savedInstanceState.getString(KEY_CURRENT_PHOTO_FILE);
+        if (fileName != null) {
+            mCurrentPhotoFile = new File(fileName);
+        }
+        mQuerySelection = savedInstanceState.getString(KEY_QUERY_SELECTION);
+        mContactIdForJoin = savedInstanceState.getLong(KEY_CONTACT_ID_FOR_JOIN);
+
         bindEditors();
 
         super.onRestoreInstanceState(savedInstanceState);
diff --git a/tests/src/com/android/contacts/EntityDeltaTests.java b/tests/src/com/android/contacts/EntityDeltaTests.java
index 70a506b..fa716c7 100644
--- a/tests/src/com/android/contacts/EntityDeltaTests.java
+++ b/tests/src/com/android/contacts/EntityDeltaTests.java
@@ -367,7 +367,7 @@
         final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
         source.buildAssert(diff);
         source.buildDiff(diff);
-        assertEquals("Unexpected operations", 1, diff.size());
+        assertEquals("Unexpected operations", 2, diff.size());
         {
             final ContentProviderOperation oper = diff.get(0);
             assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
@@ -395,7 +395,7 @@
         final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
         source.buildAssert(diff);
         source.buildDiff(diff);
-        assertEquals("Unexpected operations", 2, diff.size());
+        assertEquals("Unexpected operations", 3, diff.size());
         {
             final ContentProviderOperation oper = diff.get(0);
             assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
diff --git a/tests/src/com/android/contacts/EntityModifierTests.java b/tests/src/com/android/contacts/EntityModifierTests.java
index 4bc4622..18877a3 100644
--- a/tests/src/com/android/contacts/EntityModifierTests.java
+++ b/tests/src/com/android/contacts/EntityModifierTests.java
@@ -512,7 +512,7 @@
         // Build diff, expecting single insert
         final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
         state.buildDiff(diff);
-        assertEquals("Unexpected operations", 1, diff.size());
+        assertEquals("Unexpected operations", 2, diff.size());
         {
             final ContentProviderOperation oper = diff.get(0);
             assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
@@ -540,7 +540,7 @@
         // Build diff, expecting two insert operations
         final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
         state.buildDiff(diff);
-        assertEquals("Unexpected operations", 2, diff.size());
+        assertEquals("Unexpected operations", 3, diff.size());
         {
             final ContentProviderOperation oper = diff.get(0);
             assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
diff --git a/tests/src/com/android/contacts/EntitySetTests.java b/tests/src/com/android/contacts/EntitySetTests.java
index c9fc3fa..edfca6d 100644
--- a/tests/src/com/android/contacts/EntitySetTests.java
+++ b/tests/src/com/android/contacts/EntitySetTests.java
@@ -486,6 +486,7 @@
                 buildAssertVersion(VER_FIRST),
                 buildOper(RawContacts.CONTENT_URI, TYPE_INSERT, joeContactInsert),
                 buildOper(Data.CONTENT_URI, TYPE_INSERT, joePhoneInsert),
+                buildAggregationModeUpdate(RawContacts.AGGREGATION_MODE_DEFAULT),
                 buildUpdateAggregationKeepTogether(CONTACT_BOB));
 
         // Merge in the second version, verify that our insert remains
@@ -495,6 +496,7 @@
                 buildAssertVersion(VER_SECOND),
                 buildOper(RawContacts.CONTENT_URI, TYPE_INSERT, joeContactInsert),
                 buildOper(Data.CONTENT_URI, TYPE_INSERT, joePhoneInsert),
+                buildAggregationModeUpdate(RawContacts.AGGREGATION_MODE_DEFAULT),
                 buildUpdateAggregationKeepTogether(CONTACT_BOB));
     }
 
@@ -545,6 +547,7 @@
                 buildAssertVersion(VER_SECOND),
                 buildOper(RawContacts.CONTENT_URI, TYPE_INSERT, contactInsert),
                 buildOper(Data.CONTENT_URI, TYPE_INSERT, phoneInsert),
+                buildAggregationModeUpdate(RawContacts.AGGREGATION_MODE_DEFAULT),
                 buildUpdateAggregationKeepTogether(CONTACT_BOB));
     }