am 9f267bdd: Import revised translations
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 1fae984..762d66d 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -239,22 +239,30 @@
                 <data android:mimeType="vnd.android.cursor.item/postal-address_v2" android:host="com.android.contacts" />
                 <data android:mimeType="vnd.android.cursor.item/postal-address" android:host="contacts" />
             </intent-filter>
-
         </activity>
 
         <!-- An activity for joining contacts -->
-        <activity android:name="ContactsListActivity$JoinContactActivity"
+        <activity android:name="JoinContactActivity"
             android:theme="@style/TallTitleBarTheme"
             android:clearTaskOnLaunch="true"
         >
             <intent-filter>
-                <action android:name="com.android.contacts.action.JOIN_AGGREGATE" />
+                <action android:name="com.android.contacts.action.JOIN_CONTACT" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
 
+        <!-- An activity for selecting multiple phone numbers -->
+        <activity android:name="MultiplePhonePickerActivity">
+            <intent-filter>
+                <action android:name="com.android.contacts.action.GET_MULTIPLE_PHONES" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="vnd.android.cursor.dir/phone_v2" android:host="com.android.contacts" />
+            </intent-filter>
+        </activity>
+
         <!-- The contacts search/filter UI -->
-        <activity android:name="ContactsListActivity$ContactsSearchActivity"
+        <activity android:name="ContactsSearchActivity"
             android:theme="@style/ContactsSearchTheme"
             android:windowSoftInputMode="stateAlwaysVisible|adjustPan"
         >
@@ -363,7 +371,7 @@
         </activity>
 
         <!-- Views the details of a single contact -->
-        <activity android:name="ViewContactActivity"
+        <activity android:name=".activities.ContactDetailActivity"
             android:label="@string/viewContactTitle"
             android:theme="@style/TallTitleBarTheme">
 
@@ -376,9 +384,22 @@
             </intent-filter>
         </activity>
 
+        <!-- Shows the List and Details in two panes. This is probably temporary -->
+        <activity android:name=".activities.TwoPaneActivity"
+            android:label="Contacts Goop"
+            android:theme="@style/TallTitleBarTheme">
+
+            <intent-filter android:label="Contacts Goop">
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.LAUNCHER" />
+                <category android:name="android.intent.category.BROWSABLE" />
+            </intent-filter>
+        </activity>
+
         <!-- Edit or insert details for a contact -->
         <activity
-            android:name=".ui.EditContactActivity"
+            android:name=".activities.ContactEditActivity"
             android:windowSoftInputMode="stateHidden|adjustResize">
 
             <intent-filter android:label="@string/editContactDescription">
@@ -427,6 +448,11 @@
             />
         </activity>
 
+        <!-- Interstitial activity that shows a phone disambig dialog -->
+        <activity android:name="CallContactActivity"
+            android:theme="@android:style/Theme.Translucent">
+        </activity>
+
         <!-- Makes .ContactsListActivity the search target for any activity in Contacts -->
         <meta-data
             android:name="android.app.default_searchable"
@@ -474,6 +500,13 @@
             </intent-filter>
         </activity>
 
+        <activity android:name=".SelectAccountActivity"
+            android:theme="@style/BackgroundOnly" />
+
+        <service
+            android:name=".ImportVCardService"
+            android:exported="false" />
+
         <activity android:name=".ExportVCardActivity"
             android:theme="@style/BackgroundOnly" />
     </application>
diff --git a/res/anim/footer_appear.xml b/res/anim/footer_appear.xml
new file mode 100644
index 0000000..941454a
--- /dev/null
+++ b/res/anim/footer_appear.xml
@@ -0,0 +1,23 @@
+<?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.
+*/
+-->
+
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+    android:fromYDelta="+10%p"
+    android:toYDelta="0"
+    android:duration="300" />
\ No newline at end of file
diff --git a/res/drawable-hdpi-finger/ic_menu_display_all.png b/res/drawable-hdpi-finger/ic_menu_display_all.png
new file mode 100755
index 0000000..563083c
--- /dev/null
+++ b/res/drawable-hdpi-finger/ic_menu_display_all.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_menu_display_selected.png b/res/drawable-hdpi-finger/ic_menu_display_selected.png
new file mode 100644
index 0000000..76b2e22
--- /dev/null
+++ b/res/drawable-hdpi-finger/ic_menu_display_selected.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_menu_select.png b/res/drawable-hdpi-finger/ic_menu_select.png
new file mode 100644
index 0000000..c5bb503
--- /dev/null
+++ b/res/drawable-hdpi-finger/ic_menu_select.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_menu_unselect.png b/res/drawable-hdpi-finger/ic_menu_unselect.png
new file mode 100644
index 0000000..178f314
--- /dev/null
+++ b/res/drawable-hdpi-finger/ic_menu_unselect.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_1.9.png b/res/drawable-hdpi/appointment_indicator_leftside_1.9.png
new file mode 100644
index 0000000..b72652b
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_1.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_10.9.png b/res/drawable-hdpi/appointment_indicator_leftside_10.9.png
new file mode 100644
index 0000000..ff09049
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_10.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_11.9.png b/res/drawable-hdpi/appointment_indicator_leftside_11.9.png
new file mode 100644
index 0000000..6a2e4f2
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_11.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_12.9.png b/res/drawable-hdpi/appointment_indicator_leftside_12.9.png
new file mode 100644
index 0000000..0f19c83
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_12.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_13.9.png b/res/drawable-hdpi/appointment_indicator_leftside_13.9.png
new file mode 100644
index 0000000..7501e35
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_13.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_14.9.png b/res/drawable-hdpi/appointment_indicator_leftside_14.9.png
new file mode 100644
index 0000000..53f97a6
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_14.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_15.9.png b/res/drawable-hdpi/appointment_indicator_leftside_15.9.png
new file mode 100644
index 0000000..846f6f8
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_15.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_16.9.png b/res/drawable-hdpi/appointment_indicator_leftside_16.9.png
new file mode 100644
index 0000000..1707540
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_16.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_17.9.png b/res/drawable-hdpi/appointment_indicator_leftside_17.9.png
new file mode 100644
index 0000000..7fd945d
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_17.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_18.9.png b/res/drawable-hdpi/appointment_indicator_leftside_18.9.png
new file mode 100644
index 0000000..8cf47ae
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_18.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_19.9.png b/res/drawable-hdpi/appointment_indicator_leftside_19.9.png
new file mode 100644
index 0000000..6831c01
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_19.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_2.9.png b/res/drawable-hdpi/appointment_indicator_leftside_2.9.png
new file mode 100644
index 0000000..b4cee11
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_2.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_20.9.png b/res/drawable-hdpi/appointment_indicator_leftside_20.9.png
new file mode 100644
index 0000000..d07d826
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_20.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_21.9.png b/res/drawable-hdpi/appointment_indicator_leftside_21.9.png
new file mode 100644
index 0000000..f410269
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_21.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_3.9.png b/res/drawable-hdpi/appointment_indicator_leftside_3.9.png
new file mode 100644
index 0000000..69bd6a9
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_3.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_4.9.png b/res/drawable-hdpi/appointment_indicator_leftside_4.9.png
new file mode 100644
index 0000000..d09ea90
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_4.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_5.9.png b/res/drawable-hdpi/appointment_indicator_leftside_5.9.png
new file mode 100644
index 0000000..d27fc91
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_5.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_6.9.png b/res/drawable-hdpi/appointment_indicator_leftside_6.9.png
new file mode 100644
index 0000000..c014633
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_6.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_7.9.png b/res/drawable-hdpi/appointment_indicator_leftside_7.9.png
new file mode 100644
index 0000000..febb514
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_7.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_8.9.png b/res/drawable-hdpi/appointment_indicator_leftside_8.9.png
new file mode 100644
index 0000000..1415e44
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_8.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_9.9.png b/res/drawable-hdpi/appointment_indicator_leftside_9.9.png
new file mode 100644
index 0000000..d018fcf
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_9.9.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_menu_display_all.png b/res/drawable-mdpi-finger/ic_menu_display_all.png
new file mode 100644
index 0000000..61a9e35
--- /dev/null
+++ b/res/drawable-mdpi-finger/ic_menu_display_all.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_menu_display_selected.png b/res/drawable-mdpi-finger/ic_menu_display_selected.png
new file mode 100644
index 0000000..b4ec7a8
--- /dev/null
+++ b/res/drawable-mdpi-finger/ic_menu_display_selected.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_menu_select.png b/res/drawable-mdpi-finger/ic_menu_select.png
new file mode 100644
index 0000000..29e3d7e
--- /dev/null
+++ b/res/drawable-mdpi-finger/ic_menu_select.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_menu_unselect.png b/res/drawable-mdpi-finger/ic_menu_unselect.png
new file mode 100644
index 0000000..2b69bc8
--- /dev/null
+++ b/res/drawable-mdpi-finger/ic_menu_unselect.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_1.9.png b/res/drawable-mdpi/appointment_indicator_leftside_1.9.png
new file mode 100644
index 0000000..5e40235
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_1.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_10.9.png b/res/drawable-mdpi/appointment_indicator_leftside_10.9.png
new file mode 100644
index 0000000..d0cb144
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_10.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_11.9.png b/res/drawable-mdpi/appointment_indicator_leftside_11.9.png
new file mode 100644
index 0000000..034f496
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_11.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_12.9.png b/res/drawable-mdpi/appointment_indicator_leftside_12.9.png
new file mode 100644
index 0000000..6371b3a
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_12.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_13.9.png b/res/drawable-mdpi/appointment_indicator_leftside_13.9.png
new file mode 100644
index 0000000..a8b42c6
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_13.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_14.9.png b/res/drawable-mdpi/appointment_indicator_leftside_14.9.png
new file mode 100644
index 0000000..a69e519
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_14.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_15.9.png b/res/drawable-mdpi/appointment_indicator_leftside_15.9.png
new file mode 100644
index 0000000..5d68470
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_15.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_16.9.png b/res/drawable-mdpi/appointment_indicator_leftside_16.9.png
new file mode 100644
index 0000000..d9420c1
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_16.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_17.9.png b/res/drawable-mdpi/appointment_indicator_leftside_17.9.png
new file mode 100644
index 0000000..d0875c4
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_17.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_18.9.png b/res/drawable-mdpi/appointment_indicator_leftside_18.9.png
new file mode 100644
index 0000000..fc152f7
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_18.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_19.9.png b/res/drawable-mdpi/appointment_indicator_leftside_19.9.png
new file mode 100644
index 0000000..6506a94
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_19.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_2.9.png b/res/drawable-mdpi/appointment_indicator_leftside_2.9.png
new file mode 100644
index 0000000..3baf5cc
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_2.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_20.9.png b/res/drawable-mdpi/appointment_indicator_leftside_20.9.png
new file mode 100644
index 0000000..28340ba
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_20.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_21.9.png b/res/drawable-mdpi/appointment_indicator_leftside_21.9.png
new file mode 100644
index 0000000..5319f07
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_21.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_3.9.png b/res/drawable-mdpi/appointment_indicator_leftside_3.9.png
new file mode 100644
index 0000000..9850791
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_3.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_4.9.png b/res/drawable-mdpi/appointment_indicator_leftside_4.9.png
new file mode 100644
index 0000000..e344ccb
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_4.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_5.9.png b/res/drawable-mdpi/appointment_indicator_leftside_5.9.png
new file mode 100644
index 0000000..11b4dfb
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_5.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_6.9.png b/res/drawable-mdpi/appointment_indicator_leftside_6.9.png
new file mode 100644
index 0000000..7419d47
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_6.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_7.9.png b/res/drawable-mdpi/appointment_indicator_leftside_7.9.png
new file mode 100644
index 0000000..0a3a272
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_7.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_8.9.png b/res/drawable-mdpi/appointment_indicator_leftside_8.9.png
new file mode 100644
index 0000000..db18d27
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_8.9.png
Binary files differ
diff --git a/res/drawable-mdpi/appointment_indicator_leftside_9.9.png b/res/drawable-mdpi/appointment_indicator_leftside_9.9.png
new file mode 100644
index 0000000..5037de8
--- /dev/null
+++ b/res/drawable-mdpi/appointment_indicator_leftside_9.9.png
Binary files differ
diff --git a/res/layout-finger/contacts_list_content.xml b/res/layout-finger/contacts_list_content.xml
index 36c03ce..9ea1c68 100644
--- a/res/layout-finger/contacts_list_content.xml
+++ b/res/layout-finger/contacts_list_content.xml
@@ -23,13 +23,19 @@
         >
 
     <view
-        class="com.android.contacts.PinnedHeaderListView" 
+        class="com.android.contacts.ContactEntryListView" 
         android:id="@android:id/list"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
+        android:layout_height="0dip"
         android:fastScrollEnabled="true"
+        android:layout_weight="1"
     />
 
     <include layout="@layout/contacts_list_empty"/>
 
+    <ViewStub android:id="@+id/footer_stub"
+        android:layout="@layout/footer_panel"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+    />
 </LinearLayout>
diff --git a/res/layout-finger/contacts_list_content_join.xml b/res/layout-finger/contacts_list_content_join.xml
index b50713b..b59c240 100644
--- a/res/layout-finger/contacts_list_content_join.xml
+++ b/res/layout-finger/contacts_list_content_join.xml
@@ -61,10 +61,12 @@
         </LinearLayout>
     </LinearLayout>
 
-    <ListView android:id="@android:id/list"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:fastScrollEnabled="true"
+    <view
+        class="com.android.contacts.ContactEntryListView" 
+        android:id="@android:id/list"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:fastScrollEnabled="true"
     />
 </LinearLayout>
 
diff --git a/res/layout-finger/contacts_list_empty.xml b/res/layout-finger/contacts_list_empty.xml
index 195da1e..d655899 100644
--- a/res/layout-finger/contacts_list_empty.xml
+++ b/res/layout-finger/contacts_list_empty.xml
@@ -13,8 +13,9 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<ScrollView 
+<view 
     xmlns:android="http://schemas.android.com/apk/res/android"
+    class="com.android.contacts.ContactListEmptyView"
     android:id="@android:id/empty"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
@@ -35,6 +36,7 @@
           android:paddingRight="10dip"
           android:paddingTop="10dip"
           android:lineSpacingMultiplier="0.92"
+          android:visibility="gone"
       />
       
       <LinearLayout android:id="@+id/import_failure"
@@ -61,4 +63,4 @@
             android:text="@string/upgrade_out_of_memory_retry"/>
       </LinearLayout>
     </LinearLayout>
-</ScrollView>
+</view>
diff --git a/res/layout-finger/contacts_list_search_results.xml b/res/layout-finger/contacts_list_search_results.xml
index 244ca80..7053cb6 100644
--- a/res/layout-finger/contacts_list_search_results.xml
+++ b/res/layout-finger/contacts_list_search_results.xml
@@ -53,7 +53,7 @@
     </LinearLayout>
 
     <view
-        class="com.android.contacts.PinnedHeaderListView"
+        class="com.android.contacts.ContactEntryListView"
         android:id="@android:id/list"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
diff --git a/res/layout-finger/contacts_search_content.xml b/res/layout-finger/contacts_search_content.xml
index ae72376..480a8aa 100644
--- a/res/layout-finger/contacts_search_content.xml
+++ b/res/layout-finger/contacts_search_content.xml
@@ -26,10 +26,11 @@
     <include android:id="@+id/searchView"
         layout="@layout/search_bar"/>
 
-    <ListView
-        android:id="@android:id/list"
+    <FrameLayout
+        android:id="@+id/list_container"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
+        android:layout_height="0dip"
+        android:layout_weight="1"
         android:fastScrollEnabled="true"
         android:background="@android:color/background_dark"
     />
@@ -37,6 +38,12 @@
     <!-- Transparent filler -->
     <View android:id="@android:id/empty"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
+        android:layout_height="0dip"
+        android:layout_weight="1"
+    />
+    <ViewStub android:id="@+id/footer_stub"
+        android:layout="@layout/footer_panel"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
     />
 </LinearLayout>
diff --git a/res/layout-finger/footer_panel.xml b/res/layout-finger/footer_panel.xml
new file mode 100644
index 0000000..2625a43
--- /dev/null
+++ b/res/layout-finger/footer_panel.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/footer"
+    android:orientation="horizontal"
+    android:visibility="gone"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    android:gravity="center"
+    style="@android:style/ButtonBar"
+>
+
+    <Button android:id="@+id/done"
+        android:layout_width="0dip"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:text="@string/menu_done"
+    />
+
+    <Button android:id="@+id/revert"
+        android:layout_width="0dip"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:text="@string/menu_doNotSave"
+    />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout-finger/recent_calls_list_child_item.xml b/res/layout-finger/recent_calls_list_child_item.xml
index 14eb24d..527e259 100644
--- a/res/layout-finger/recent_calls_list_child_item.xml
+++ b/res/layout-finger/recent_calls_list_child_item.xml
@@ -21,7 +21,7 @@
     android:background="@drawable/list_item_background_secondary"
 >
 
-    <com.android.contacts.ui.widget.DontPressWithParentImageView android:id="@+id/call_icon"
+    <ImageView android:id="@+id/call_icon"
         android:layout_width="wrap_content"
         android:layout_height="match_parent"
         android:paddingLeft="14dip"
diff --git a/res/layout-finger/recent_calls_list_group_item.xml b/res/layout-finger/recent_calls_list_group_item.xml
index 2d3e7af..5b4cf21 100644
--- a/res/layout-finger/recent_calls_list_group_item.xml
+++ b/res/layout-finger/recent_calls_list_group_item.xml
@@ -20,7 +20,7 @@
     android:paddingLeft="7dip"
 >
 
-    <com.android.contacts.ui.widget.DontPressWithParentImageView android:id="@+id/call_icon"
+    <ImageView android:id="@+id/call_icon"
         android:layout_width="wrap_content"
         android:layout_height="match_parent"
         android:paddingLeft="14dip"
diff --git a/res/layout-finger/recent_calls_list_item.xml b/res/layout-finger/recent_calls_list_item.xml
index 8efa23c..2c519d6 100644
--- a/res/layout-finger/recent_calls_list_item.xml
+++ b/res/layout-finger/recent_calls_list_item.xml
@@ -20,7 +20,7 @@
     android:paddingLeft="7dip"
 >
 
-    <com.android.contacts.ui.widget.DontPressWithParentImageView android:id="@+id/call_icon"
+    <ImageView android:id="@+id/call_icon"
         android:layout_width="wrap_content"
         android:layout_height="match_parent"
         android:paddingLeft="14dip"
diff --git a/res/layout-finger/search_bar.xml b/res/layout-finger/search_bar.xml
index d322948..304c35d 100644
--- a/res/layout-finger/search_bar.xml
+++ b/res/layout-finger/search_bar.xml
@@ -52,7 +52,7 @@
                 android:scaleType="centerInside" />
               
             <view
-                class="com.android.contacts.SearchEditText"
+                class="com.android.contacts.widget.SearchEditText"
                 android:id="@+id/search_src_text"
                 android:layout_height="wrap_content"
                 android:layout_width="0dip"
diff --git a/res/layout/contact_detail.xml b/res/layout/contact_detail.xml
new file mode 100644
index 0000000..10d3e48
--- /dev/null
+++ b/res/layout/contact_detail.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+  
+          http://www.apache.org/licenses/LICENSE-2.0
+  
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/contact_detail"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    
+    <com.android.internal.widget.ContactHeaderWidget
+        android:id="@+id/contact_header_widget"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"/>
+        
+    <ListView android:id="@android:id/list"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@drawable/title_bar_shadow"
+    />
+    
+    <ScrollView android:id="@android:id/empty"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:fillViewport="true"
+    >
+        <TextView android:id="@+id/emptyText"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/no_contact_details"
+            android:textSize="20sp"
+            android:textColor="?android:attr/textColorSecondary"
+            android:paddingLeft="10dip"
+            android:paddingRight="10dip"
+            android:paddingTop="10dip"
+            android:lineSpacingMultiplier="0.92"
+        />
+    </ScrollView>
+            
+</LinearLayout>
+
diff --git a/res/layout/contact_detail_activity.xml b/res/layout/contact_detail_activity.xml
new file mode 100644
index 0000000..9b63fec
--- /dev/null
+++ b/res/layout/contact_detail_activity.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <fragment android:name="com.android.contacts.views.detail.ContactDetailFragment"
+            android:id="@+id/contact_detail_fragment"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="1" />
+</LinearLayout>
diff --git a/res/layout/contact_edit.xml b/res/layout/contact_edit.xml
new file mode 100644
index 0000000..25dff3f
--- /dev/null
+++ b/res/layout/contact_edit.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 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:id="@+id/contact_edit"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+>
+
+    <ScrollView
+        android:layout_width="match_parent"
+        android:layout_height="1px"
+        android:layout_weight="1"
+        android:fillViewport="true"
+    >
+
+        <LinearLayout android:id="@+id/editors"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:orientation="vertical"
+        />
+
+    </ScrollView>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        style="@android:style/ButtonBar"
+    >
+
+        <Button android:id="@+id/btn_done"
+            android:layout_width="0dip"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:text="@string/menu_done"
+        />
+
+        <Button android:id="@+id/btn_discard"
+            android:layout_width="0dip"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:text="@string/menu_doNotSave"
+        />
+
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/res/layout/contact_edit_activity.xml b/res/layout/contact_edit_activity.xml
new file mode 100644
index 0000000..d01d031
--- /dev/null
+++ b/res/layout/contact_edit_activity.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <fragment android:name="com.android.contacts.views.edit.ContactEditFragment"
+            android:id="@+id/contact_edit_fragment"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="1" />
+</LinearLayout>
diff --git a/res/layout/item_contact_editor.xml b/res/layout/item_contact_editor.xml
index f5e7bd3..57d641f 100644
--- a/res/layout/item_contact_editor.xml
+++ b/res/layout/item_contact_editor.xml
@@ -14,7 +14,7 @@
      limitations under the License.
 -->
 
-<!-- placed inside act_edit as tabcontent -->
+<!-- placed inside act_edit -->
 <com.android.contacts.ui.widget.ContactEditorView
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
@@ -22,14 +22,6 @@
     android:orientation="horizontal"
 >
 
-    <!-- Left side color bar -->
-    <ImageView
-        android:id="@+id/color_bar"
-        android:layout_width="4dip"
-        android:layout_height="match_parent"
-        android:visibility="gone"
-    />
-
     <!-- The content -->
     <LinearLayout
         android:layout_width="0dip"
@@ -116,65 +108,28 @@
             android:layout_marginBottom="4dip"
             layout="@layout/item_generic_editor" />
 
-        <TextView android:id="@+id/read_only_name"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="6dip"
-            android:layout_marginBottom="6dip"
-            android:layout_marginLeft="10dip"
-
-            android:textAppearance="?android:attr/textAppearanceLarge"
-        />
-
         <LinearLayout
-            android:id="@+id/sect_general"
+            android:id="@+id/sect_fields"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:orientation="vertical"
         />
 
-        <View android:id="@+id/head_secondary_divider"
+        <View
             android:layout_width="match_parent"
             android:layout_height="1px"
-            android:background="?android:attr/listDivider" />
-
-        <TextView
-            android:id="@+id/head_secondary"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-
-            android:gravity="center_vertical"
-            android:minHeight="?android:attr/listPreferredItemHeight"
-            android:text="@string/edit_secondary_collapse"
-            android:textAppearance="?android:attr/textAppearanceMedium"
-            android:textColor="@color/kind_title"
-            android:singleLine="true"
-            android:ellipsize="marquee"
-            android:focusable="true"
-            android:clickable="true"
-            android:paddingLeft="10dip"
-            android:drawablePadding="10dip" />
-
-        <LinearLayout
-            android:id="@+id/sect_secondary"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:orientation="vertical" />
-
-        <TextView
-            android:id="@+id/edit_read_only"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="10dip"
-            android:layout_marginBottom="10dip"
-            android:layout_marginLeft="10dip"
-
-            android:textAppearance="?android:attr/textAppearanceSmall"
-            android:textColor="?android:attr/textColorPrimary"
-            android:drawableLeft="@android:drawable/ic_dialog_alert"
-            android:drawablePadding="10dip"
+            android:layout_alignParentBottom="true"
+            android:background="?android:attr/listDivider"
         />
 
+        <Button
+            android:id="@+id/button_add_field"
+            android:text="@string/add_field"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="right"
+            android:layout_marginTop="10dip"
+        />
     </LinearLayout>
 
 </com.android.contacts.ui.widget.ContactEditorView>
diff --git a/res/layout/item_generic_editor.xml b/res/layout/item_generic_editor.xml
index e672eba..329ad28 100644
--- a/res/layout/item_generic_editor.xml
+++ b/res/layout/item_generic_editor.xml
@@ -48,21 +48,12 @@
         android:gravity="center_vertical" />
 
     <ImageButton
-        android:id="@+id/edit_more"
+        android:id="@+id/edit_more_or_less"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_alignParentRight="true"
         android:layout_alignBottom="@id/edit_fields"
         android:visibility="gone"
-        style="@style/MoreButton" />
-
-    <ImageButton
-        android:id="@+id/edit_less"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_alignParentRight="true"
-        android:layout_alignBottom="@id/edit_fields"
-        android:visibility="gone"
-        style="@style/LessButton" />
+        style="@style/EmptyButton" />
 
 </com.android.contacts.ui.widget.GenericEditorView>
diff --git a/res/layout/item_kind_section.xml b/res/layout/item_kind_section.xml
index d1dec5e..a78896b 100644
--- a/res/layout/item_kind_section.xml
+++ b/res/layout/item_kind_section.xml
@@ -25,7 +25,7 @@
 
     <View
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
+        android:layout_height="1px"
         android:background="?android:attr/listDivider" />
 
     <LinearLayout
diff --git a/res/layout/status_bar_ongoing_event_progress_bar.xml b/res/layout/status_bar_ongoing_event_progress_bar.xml
new file mode 100644
index 0000000..d1bce1a
--- /dev/null
+++ b/res/layout/status_bar_ongoing_event_progress_bar.xml
@@ -0,0 +1,111 @@
+<?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.
+ /
+TODO: This is copied from DownloadProvider, and looks similar to Bluetooth's.
+      It might be better to have this in framework.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:background="@android:drawable/status_bar_item_app_background"
+    >
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="horizontal"
+        >
+        
+        <LinearLayout
+            android:layout_width="40dp"
+            android:layout_height="match_parent"
+            android:orientation="vertical"
+            android:paddingTop="8dp"
+            android:focusable="true"
+            android:clickable="true"
+            >
+            <com.android.server.status.AnimatedImageView 
+                android:id="@+id/appIcon"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:src="@android:drawable/sym_def_app_icon"
+                />
+            <TextView android:id="@+id/progress_text" 
+                android:layout_width="wrap_content" 
+                android:layout_height="wrap_content"
+                android:textColor="#ff000000"
+                android:singleLine="true"
+                android:textSize="14sp"
+                android:layout_gravity="center_horizontal"
+                />
+        </LinearLayout>
+
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:orientation="vertical"
+            android:focusable="true"
+            android:clickable="true"
+            >
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal"
+                android:focusable="true"
+                android:clickable="true"
+                android:layout_alignParentTop="true"
+                android:paddingTop="10dp"
+                >
+                <TextView android:id="@+id/title"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:singleLine="true"
+                    android:textSize="18sp"
+                    android:textColor="#ff000000"
+                    android:paddingLeft="2dp"
+                    />
+                <TextView android:id="@+id/description"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:textColor="#ff000000"
+                    android:singleLine="true"
+                    android:textSize="14sp"
+                    android:paddingLeft="5dp"
+                    />
+            </LinearLayout>
+            <ProgressBar android:id="@+id/progress_bar"
+                style="?android:attr/progressBarStyleHorizontal"
+                android:layout_width="match_parent" 
+                android:layout_height="wrap_content"
+                android:layout_alignParentBottom="true"
+                android:paddingBottom="8dp"
+                android:paddingRight="25dp"
+                />
+        </RelativeLayout>
+    </LinearLayout>
+        
+    <com.android.server.status.AnimatedImageView 
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:src="@android:drawable/divider_horizontal_bright"
+        />
+
+</LinearLayout>
+
diff --git a/res/layout/two_pane_activity.xml b/res/layout/two_pane_activity.xml
new file mode 100644
index 0000000..bcf0ad8
--- /dev/null
+++ b/res/layout/two_pane_activity.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/two_pane_activity"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <include android:id="@+id/searchView"
+        layout="@layout/search_bar"/>
+
+    <LinearLayout
+        android:id="@+id/two_pane_activity"
+        android:orientation="horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <fragment android:name="com.android.contacts.list.DefaultContactBrowseListFragment"
+                android:id="@+id/two_pane_list"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_weight="1" />
+
+        <fragment android:name="com.android.contacts.views.detail.ContactDetailFragment"
+                android:id="@+id/two_pane_detail"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_weight="1" />
+  </LinearLayout>
+</LinearLayout>
diff --git a/res/menu/pick.xml b/res/menu/pick.xml
new file mode 100644
index 0000000..5302dd9
--- /dev/null
+++ b/res/menu/pick.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item
+        android:id="@+id/menu_display_selected"
+        android:icon="@drawable/ic_menu_display_selected"
+        android:title="@string/menu_display_selected" />
+
+    <item
+        android:id="@+id/menu_display_all"
+        android:icon="@drawable/ic_menu_display_all"
+        android:title="@string/menu_display_all" />
+
+    <item
+        android:id="@+id/menu_select_all"
+        android:icon="@drawable/ic_menu_select"
+        android:title="@string/menu_select_all" />
+
+    <item
+        android:id="@+id/menu_select_none"
+        android:icon="@drawable/ic_menu_unselect"
+        android:title="@string/menu_select_none" />
+
+</menu>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 4a7a743..3bc7ff6 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -39,4 +39,8 @@
     <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>    
+    <dimen name="list_item_header_chip_width">4dip</dimen>
+    <dimen name="list_item_header_chip_right_margin">4dip</dimen>
+    <dimen name="list_item_header_checkbox_margin">5dip</dimen>
+
 </resources>
diff --git a/res/values/ids.xml b/res/values/ids.xml
index ceb10f8..3da8741 100644
--- a/res/values/ids.xml
+++ b/res/values/ids.xml
@@ -14,12 +14,32 @@
      limitations under the License.
 -->
 <resources>
-    <!-- The EditText for entries in the EditContactActivity -->
+    <!-- Dialogs in ContactDetailFragment -->
+    <item type="id" name="detail_dialog_confirm_delete" />
+    <item type="id" name="detail_dialog_confirm_readonly_delete" />
+    <item type="id" name="detail_dialog_confirm_multiple_delete" />
+    <item type="id" name="detail_dialog_confirm_readonly_hide" />
+
+    <!-- The EditText for entries in the ContactEditFragment -->
     <item type="id" name="data"/>
     <item type="id" name="header_phones"/>
     <item type="id" name="dialog_sync_add"/>
     <item type="id" name="dialog_import_export"/>
 
+    <!-- Dialogs in ContactEditFragment -->
+    <item type="id" name="edit_dialog_confirm_delete"/>
+    <item type="id" name="edit_dialog_confirm_readonly_delete"/>
+    <item type="id" name="edit_dialog_confirm_multiple_delete"/>
+    <item type="id" name="edit_dialog_confirm_readonly_hide"/>
+    <item type="id" name="edit_dialog_pick_photo"/>
+    <item type="id" name="edit_dialog_split"/>
+    <item type="id" name="edit_dialog_select_account"/>
+
+    <!-- Request Codes (startActivityForResult) -->
+    <item type="id" name="edit_request_code_join"/>
+    <item type="id" name="edit_request_code_camera_with_data"/>
+    <item type="id" name="edit_request_code_photo_picked_with_data"/>
+
     <!-- For ImportVCardActivity -->
     <item type="id" name="dialog_searching_vcard"/>
     <item type="id" name="dialog_sdcard_not_found"/>
@@ -37,7 +57,7 @@
     <item type="id" name="dialog_multiple_contact_delete_confirmation"/>
     <item type="id" name="dialog_readonly_contact_delete_confirmation"/>
 
-    <!-- For ExportVCard -->
+    <!-- For ExportVCardActivity -->
     <item type="id" name="dialog_export_confirmation"/>
     <item type="id" name="dialog_exporting_vcard"/>
     <item type="id" name="dialog_fail_to_export_with_reason"/>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 31e64c3..e95dc3c 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -424,7 +424,7 @@
     <string name="noContactsHelpText">"You don't have any contacts to display.\n\nTo add contacts, press <font fgcolor="#ffffffff"><b>Menu</b></font> and touch:\n
         \n<li><font fgcolor="#ffffffff"><b>Accounts</b></font> to add or configure an account with contacts you can sync to the phone\n</li>
         \n<li><font fgcolor="#ffffffff"><b>New contact</b></font> to create a new contact from scratch\n</li>
-        \n<li><font fgcolor="#ffffffff"><b>Import/Export</b></font>\n</li>"
+        \n<li><font fgcolor="#ffffffff"><b>Import/Export</b></font> to import contacts from your SIM or SD card\n</li>"
     </string>
 
     <!-- Displayed full screen when the user has no contacts and they are displaying the My Contacts group, and contact syncing is enabled -->
@@ -432,14 +432,14 @@
         \n<li><font fgcolor="#ffffffff"><b>Accounts</b></font> to add or configure an account with contacts you can sync to the phone\n</li>
         \n<li><font fgcolor="#ffffffff"><b>Display options</b></font> to change which contacts are visible\n</li>
         \n<li><font fgcolor="#ffffffff"><b>New contact</b></font> to create a new contact from scratch\n</li>
-        \n<li><font fgcolor="#ffffffff"><b>Import/Export</b></font>\n</li>"
+        \n<li><font fgcolor="#ffffffff"><b>Import/Export</b></font> to import contacts from your SIM or SD card\n</li>"
     </string>
 
     <!-- Displayed full screen when the user has no contacts and they are displaying the My Contacts group, and contact syncing is disabled, and there is no sim card (cdma)-->
     <string name="noContactsNoSimHelpText">"You don't have any contacts to display.\n\nTo add contacts, press <font fgcolor="#ffffffff"><b>Menu</b></font> and touch:\n
         \n<li><font fgcolor="#ffffffff"><b>Accounts</b></font> to add or configure an account with contacts you can sync to the phone\n</li>
         \n<li><font fgcolor="#ffffffff"><b>New contact</b></font> to create a new contact from scratch\n</li>
-        \n<li><font fgcolor="#ffffffff"><b>Import/Export</b></font>\n</li>"
+        \n<li><font fgcolor="#ffffffff"><b>Import/Export</b></font> to import contacts from your SD card\n</li>"
     </string>
 
     <!-- Displayed full screen when the user has no contacts and they are displaying the My Contacts group, and contact syncing is enabled, and there is no sim card (cdma) -->
@@ -447,7 +447,7 @@
         \n<li><font fgcolor="#ffffffff"><b>Accounts</b></font> to add or configure an account with contacts you can sync to the phone\n</li>
         \n<li><font fgcolor="#ffffffff"><b>Display options</b></font> to change which contacts are visible\n</li>
         \n<li><font fgcolor="#ffffffff"><b>New contact</b></font> to create a new contact from scratch\n</li>
-        \n<li><font fgcolor="#ffffffff"><b>Import/Export</b></font>\n</li>"
+        \n<li><font fgcolor="#ffffffff"><b>Import/Export</b></font> to import contacts from your SD card\n</li>"
     </string>
 
     <!-- Displayed full screen when the user has no favorites and they are displaying the favorites tab -->
@@ -512,7 +512,7 @@
     <string name="returnCall">Return call</string>
 
     <!-- A nicely formatted call duration displayed when viewing call details. For example "42 mins 28 secs" -->
-    <string name="callDetailsDurationFormat"><xliff:g id="minutes" example="42">%1$s</xliff:g> mins <xliff:g id="seconds" example="28">%2$s</xliff:g> secs</string>
+    <string name="callDetailsDurationFormat"><xliff:g id="minutes" example="42">%s</xliff:g> mins <xliff:g id="seconds" example="28">%s</xliff:g> secs</string>
 
     <!-- A list separator for the Favorites tab indicating that items below it are frequently contacted contacts rather than starred contacts -->
     <string name="favoritesFrquentSeparator">Frequently contacted</string>
@@ -658,7 +658,7 @@
     <!-- Dialog message shown when SDcard does not exist -->
     <string name="no_sdcard_message">No SD card detected</string>
 
-    <!-- Dialog title shown when searching VCard data from SD Card -->
+    <!-- Dialog title shown when searching vCard data from SD Card -->
     <string name="searching_vcard_title">Searching for vCard</string>
 
     <!-- Action string for selecting SIM for importing contacts -->
@@ -688,13 +688,13 @@
          than one vCard files available in the system. -->
     <string name="import_all_vcard_string">Import all vCard files</string>
 
-    <!-- Dialog message shown when searching VCard data from SD Card -->
+    <!-- Dialog message shown when searching vCard data from SD Card -->
     <string name="searching_vcard_message">Searching for vCard data on SD card</string>
 
-    <!-- Dialog title shown when scanning VCard data failed. -->
+    <!-- Dialog title shown when scanning vCard data failed. -->
     <string name="scanning_sdcard_failed_title">Scanning SD card failed</string>
 
-    <!-- Dialog message shown when searching VCard data failed.
+    <!-- Dialog message shown when searching vCard data failed.
          An exact reason for the failure should -->
     <string name="scanning_sdcard_failed_message">Scanning SD card failed (Reason: \"<xliff:g id="fail_reason">%s</xliff:g>\")</string>
 
@@ -724,28 +724,35 @@
     <!-- The failed reason which should not be shown but it may in some buggy condition. -->
     <string name="fail_reason_unknown">Unknown error</string>
 
-    <!-- Dialog title shown when a user is asked to select VCard file -->
+    <!-- Dialog title shown when a user is asked to select vCard file -->
     <string name="select_vcard_title">Select vCard file</string>
 
-    <!-- The message shown while reading a vCard file/entry. The first argument is like
-    "Reading VCard" or "Reading VCard files" and the second is the display name of the current
-    data being parsed -->
-    <string name="progress_shower_message"><xliff:g id="action" example="Reading VCard">%1$s</xliff:g>\n<xliff:g id="filename" example="foo.vcf">%2$s</xliff:g></string>
+    <!-- The message shown while reading vCard(s).
+         First argument is current index of contacts to be imported.
+         Second argument is the total number of contacts.
+         Third argument is the Uri which is being read. -->
+    <string name="progress_notifier_message"><xliff:g id="current_number">%s</xliff:g>/<xliff:g id="total_number">%s</xliff:g>: <xliff:g id="filename" example="foo.vcf">%s</xliff:g></string>
 
-    <!-- Dialog title shown when reading VCard data -->
-    <string name="reading_vcard_title">Reading vCard</string>
+    <!-- Dialog title shown when reading vCard data -->
+    <string name="reading_vcard_title">Reading vCard(s)</string>
 
-    <!-- Dialog message shown when reading a VCard file -->
-    <string name="reading_vcard_message">Reading vCard file(s)</string>
+    <!-- Dialog title shown when reading vCard data failed -->
+    <string name="reading_vcard_failed_title">Failed to Read vCard data</string>
 
-    <!-- Dialog title shown when reading VCard data failed -->
-    <string name="reading_vcard_failed_title">Reading of vCard data has failed</string>
+    <!-- The title shown when reading vCard is canceled (probably by a user) -->
+    <string name="reading_vcard_canceled_title">Reading vCard data was canceled</string>
 
-    <!-- Message while reading one vCard file "(current number) of (total number) contacts" The order of "current number" and "total number" cannot be changed (like "total: (total number), current: (current number)")-->
-    <string name="reading_vcard_contacts"><xliff:g id="current_number">%1$s</xliff:g> of <xliff:g id="total_number">%2$s</xliff:g> contacts</string>
+    <!-- The title shown when reading vCard is canceled (probably by a user) -->
+    <string name="reading_vcard_finished_title">Finished Reading vCard data</string>
 
-    <!-- Message while reading multiple vCard files "(current number) of (total number) files" The order of "current number" and "total number" cannot be changed (like "total: (total number), current: (current number)")-->
-    <string name="reading_vcard_files"><xliff:g id="current_number">%1$s</xliff:g> of <xliff:g id="total_number">%2$s</xliff:g> files</string>
+    <!-- The message shown when vCard importer started running. -->
+    <string name="vcard_importer_start_message">vCard importer started.</string>
+
+    <!-- The message shown when additional vCard to be imported is given during the import for others -->
+    <string name="vcard_importer_will_start_message">vCard importer will import the vCard after a while.</string>
+
+    <!-- The percentage, used for expressing the progress of vCard import. -->
+    <string name="percentage">%s%%</string>
 
     <!-- Dialog title shown when a user confirms whether he/she export Contact data -->
     <string name="confirm_export_title">Confirm export</string>
@@ -803,10 +810,10 @@
 
     <!-- The failed reason shown when vCard importer/exporter could not open the file
          specified by a user. The file name should be in the message. -->
-    <string name="fail_reason_could_not_open_file">Could not open \"<xliff:g id="file_name">%1$s</xliff:g>\": <xliff:g id="exact_reason">%2$s</xliff:g></string>
+    <string name="fail_reason_could_not_open_file">Could not open \"<xliff:g id="file_name">%s</xliff:g>\": <xliff:g id="exact_reason">%s</xliff:g></string>
 
     <!-- Message in progress bar while exporting contact list to a file "(current number) of (total number) contacts" The order of "current number" and "total number" cannot be changed (like "total: (total number), current: (current number)")-->
-    <string name="exporting_contact_list_progress"><xliff:g id="current_number">%1$s</xliff:g> of <xliff:g id="total_number">%2$s</xliff:g> contacts</string>
+    <string name="exporting_contact_list_progress"><xliff:g id="current_number">%s</xliff:g> of <xliff:g id="total_number">%s</xliff:g> contacts</string>
 
     <!-- The string used to describe Contacts as a searchable item within system search settings. -->
     <string name="search_settings_description">Names of your contacts</string>
@@ -1134,4 +1141,36 @@
 
     <!-- Title shown in the search result activity of contacts app while searching -->
     <string name="search_results_searching">Searching...</string>
+
+    <!-- Message of progress dialog for multiple picker -->
+    <string name="adding_recipients">"Loading \u2026"</string>
+
+    <!-- Label to display only selection in multiple picker -->
+    <string name="menu_display_selected">"Show selected"</string>
+
+    <!-- Label to display all recipients in multiple picker -->
+    <string name="menu_display_all">"Show all"</string>
+
+    <!-- Label to select all contacts in multiple picker -->
+    <string name="menu_select_all">"Select all"</string>
+
+    <!-- Label to clear all selection in multiple picker -->
+    <string name="menu_select_none">"Unselect all"</string>
+
+    <!-- Label to display how many selected in multiple picker -->
+    <plurals name="multiple_picker_title">
+        <!-- number of selected recipients is one -->
+        <item quantity="one">"1 recipient selected"</item>
+        <!-- number of selected recipients is not equal to one -->
+        <item quantity="other"><xliff:g id="count">%d</xliff:g>" recipients selected"</item>
+    </plurals>
+
+    <!-- Separator label to display unknown recipients in multiple picker -->
+    <string name="unknown_contacts_separator">"Unknown contacts"</string>
+
+    <!-- The text displayed when the contacts list is empty while displaying only selected contacts in multiple picker -->
+    <string name="no_contacts_selected">"No contacts selected."</string>
+
+    <!-- The add field button shown in the editor under each editable Raw Contact -->
+    <string name="add_field">Add information</string>
 </resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index ad4f4f6..93ae91d 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -24,6 +24,10 @@
         <item name="android:windowAnimationStyle">@style/ContactsSearchAnimation</item>
     </style>
 
+    <style name="EmptyButton">
+        <item name="android:background">@drawable/btn_circle</item>
+    </style>
+
     <style name="MinusButton">
         <item name="android:background">@drawable/btn_circle</item>
         <item name="android:src">@drawable/ic_btn_round_minus</item>
diff --git a/src/android/app/patterns/AsyncTaskLoader.java b/src/android/app/patterns/AsyncTaskLoader.java
new file mode 100644
index 0000000..84effaf
--- /dev/null
+++ b/src/android/app/patterns/AsyncTaskLoader.java
@@ -0,0 +1,94 @@
+/*
+ * 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 android.app.patterns;
+
+import android.content.Context;
+import android.os.AsyncTask;
+
+/**
+ * Abstract Loader that provides an {@link AsyncTask} to do the work.
+ *
+ * @param <D> the data type to be loaded.
+ */
+public abstract class AsyncTaskLoader<D> extends Loader<D> {
+    final class LoadListTask extends AsyncTask<Void, Void, D> {
+        /* Runs on a worker thread */
+        @Override
+        protected D doInBackground(Void... params) {
+            return AsyncTaskLoader.this.loadInBackground();
+        }
+
+        /* Runs on the UI thread */
+        @Override
+        protected void onPostExecute(D data) {
+            AsyncTaskLoader.this.dispatchOnLoadComplete(data);
+        }
+    }
+
+    private LoadListTask mTask;
+
+    public AsyncTaskLoader(Context context) {
+        super(context);
+    }
+
+    /**
+     * Force an asynchronous load. Unlike {@link #startLoading()} this will ignore a previously
+     * loaded data set and load a new one.
+     */
+    @Override
+    public void forceLoad() {
+        mTask = new LoadListTask();
+        mTask.execute((Void[]) null);
+    }
+
+    /**
+     * Attempt to cancel the current load task. See {@link AsyncTask#cancel(boolean)}
+     * for more info.
+     *
+     * @return <tt>false</tt> if the task could not be cancelled,
+     *         typically because it has already completed normally, or
+     *         because {@link startLoading()} hasn't been called, and
+     *         <tt>true</tt> otherwise
+     */
+    public boolean cancelLoad() {
+        if (mTask != null) {
+            return mTask.cancel(false);
+        }
+        return false;
+    }
+
+    private void dispatchOnLoadComplete(D data) {
+        mTask = null;
+        onLoadComplete(data);
+    }
+
+    /**
+     * Called on a worker thread to perform the actual load. Implementions should not deliver the
+     * results directly, but should return them from this this method and deliver them from
+     * {@link #onPostExecute()}
+     *
+     * @return the result of the load
+     */
+    protected abstract D loadInBackground();
+
+    /**
+     * Called on the UI thread with the result of the load.
+     *
+     * @param data the result of the load
+     */
+    protected abstract void onLoadComplete(D data);
+}
diff --git a/src/android/app/patterns/CursorLoader.java b/src/android/app/patterns/CursorLoader.java
new file mode 100644
index 0000000..04b58f5
--- /dev/null
+++ b/src/android/app/patterns/CursorLoader.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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 android.app.patterns;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+
+public class CursorLoader extends AsyncTaskLoader<Cursor> {
+    Cursor mCursor;
+    ForceLoadContentObserver mObserver;
+    boolean mStopped;
+    Uri mUri;
+    String[] mProjection;
+    String mSelection;
+    String[] mSelectionArgs;
+    String mSortOrder;
+
+    /* Runs on a worker thread */
+    @Override
+    protected Cursor loadInBackground() {
+        Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection,
+                mSelectionArgs, mSortOrder);
+        // Ensure the cursor window is filled
+        if (cursor != null) {
+            cursor.getCount();
+            cursor.registerContentObserver(mObserver);
+        }
+        return cursor;
+    }
+
+    /* Runs on the UI thread */
+    @Override
+    protected void onLoadComplete(Cursor cursor) {
+        if (mStopped) {
+            // An async query came in while the loader is stopped
+            cursor.close();
+            return;
+        }
+        mCursor = cursor;
+        deliverResult(cursor);
+    }
+
+    public CursorLoader(Context context, Uri uri, String[] projection, String selection,
+            String[] selectionArgs, String sortOrder) {
+        super(context);
+        mObserver = new ForceLoadContentObserver();
+        mUri = uri;
+        mProjection = projection;
+        mSelection = selection;
+        mSelectionArgs = selectionArgs;
+        mSortOrder = sortOrder;
+    }
+
+    /**
+     * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks
+     * will be called on the UI thread. If a previous load has been completed and is still valid
+     * the result may be passed to the callbacks immediately.
+     *
+     * Must be called from the UI thread
+     */
+    @Override
+    public void startLoading() {
+        mStopped = false;
+
+        if (mCursor != null) {
+            deliverResult(mCursor);
+        } else {
+            forceLoad();
+        }
+    }
+
+    /**
+     * Must be called from the UI thread
+     */
+    @Override
+    public void stopLoading() {
+        if (mCursor != null && !mCursor.isClosed()) {
+            mCursor.close();
+            mCursor = null;
+        }
+
+        // Attempt to cancel the current load task if possible.
+        cancelLoad();
+
+        // Make sure that any outstanding loads clean themselves up properly
+        mStopped = true;
+    }
+
+    @Override
+    public void destroy() {
+        // Ensure the loader is stopped
+        stopLoading();
+    }
+
+    public Uri getUri() {
+        return mUri;
+    }
+
+    public void setUri(Uri uri) {
+        mUri = uri;
+    }
+
+    public String[] getProjection() {
+        return mProjection;
+    }
+
+    public void setProjection(String[] projection) {
+        mProjection = projection;
+    }
+
+    public String getSelection() {
+        return mSelection;
+    }
+
+    public void setSelection(String selection) {
+        mSelection = selection;
+    }
+
+    public String[] getSelectionArgs() {
+        return mSelectionArgs;
+    }
+
+    public void setSelectionArgs(String[] selectionArgs) {
+        mSelectionArgs = selectionArgs;
+    }
+
+    public String getSortOrder() {
+        return mSortOrder;
+    }
+
+    public void setSortOrder(String sortOrder) {
+        mSortOrder = sortOrder;
+    }
+}
diff --git a/src/android/app/patterns/Loader.java b/src/android/app/patterns/Loader.java
new file mode 100644
index 0000000..18c64f3
--- /dev/null
+++ b/src/android/app/patterns/Loader.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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 android.app.patterns;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.Handler;
+
+public abstract class Loader<D> {
+    private int mId;
+    private OnLoadCompleteListener<D> mListener;
+    private Context mContext;
+
+    protected final class ForceLoadContentObserver extends ContentObserver {
+        public ForceLoadContentObserver() {
+            super(new Handler());
+        }
+
+        @Override
+        public boolean deliverSelfNotifications() {
+            return true;
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            forceLoad();
+        }
+    }
+
+    public interface OnLoadCompleteListener<D> {
+        /**
+         * Called on the thread that created the Loader when the load is complete.
+         *
+         * @param loader the loader that completed the load
+         * @param data the result of the load
+         */
+        public void onLoadComplete(Loader<D> loader, D data);
+    }
+
+    /**
+     * Sends the result of the load to the register listener.
+     *
+     * @param data the result of the load
+     */
+    protected void deliverResult(D data) {
+        if (mListener != null) {
+            mListener.onLoadComplete(this, data);
+        }
+    }
+
+    /**
+     * Stores away the application context associated with context. Since Loaders can be used
+     * across multiple activities it's dangerous to store the context directly.
+     *
+     * @param context used to retrieve the application context.
+     */
+    public Loader(Context context) {
+        mContext = context.getApplicationContext();
+    }
+
+    /**
+     * @return an application context retrieved from the Context passed to the constructor.
+     */
+    public Context getContext() {
+        return mContext;
+    }
+
+    /**
+     * @return the ID of this loader
+     */
+    public int getId() {
+        return mId;
+    }
+
+    /**
+     * Registers a class that will receive callbacks when a load is complete. The callbacks will
+     * be called on the UI thread so it's safe to pass the results to widgets.
+     *
+     * Must be called from the UI thread
+     */
+    public void registerListener(int id, OnLoadCompleteListener<D> listener) {
+        if (mListener != null) {
+            throw new IllegalStateException("There is already a listener registered");
+        }
+        mListener = listener;
+        mId = id;
+    }
+
+    /**
+     * Must be called from the UI thread
+     */
+    public void unregisterListener(OnLoadCompleteListener<D> listener) {
+        if (mListener == null) {
+            throw new IllegalStateException("No listener register");
+        }
+        if (mListener != listener) {
+            throw new IllegalArgumentException("Attempting to unregister the wrong listener");
+        }
+        mListener = null;
+    }
+
+    /**
+     * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks
+     * will be called on the UI thread. If a previous load has been completed and is still valid
+     * the result may be passed to the callbacks immediately. The loader will monitor the source of
+     * the data set and may deliver future callbacks if the source changes. Calling
+     * {@link #stopLoading} will stop the delivery of callbacks.
+     *
+     * Must be called from the UI thread
+     */
+    public abstract void startLoading();
+
+    /**
+     * Force an asynchronous load. Unlike {@link #startLoading()} this will ignore a previously
+     * loaded data set and load a new one.
+     */
+    public abstract void forceLoad();
+
+    /**
+     * Stops delivery of updates until the next time {@link #startLoading()} is called
+     *
+     * Must be called from the UI thread
+     */
+    public abstract void stopLoading();
+
+    /**
+     * Destroys the loader and frees it's resources, making it unusable.
+     *
+     * Must be called from the UI thread
+     */
+    public abstract void destroy();
+}
\ No newline at end of file
diff --git a/src/android/app/patterns/LoaderActivity.java b/src/android/app/patterns/LoaderActivity.java
new file mode 100644
index 0000000..bcb3692
--- /dev/null
+++ b/src/android/app/patterns/LoaderActivity.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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 android.app.patterns;
+
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import java.util.HashMap;
+
+/**
+ * The idea here was to abstract the generic life cycle junk needed to properly keep loaders going.
+ * It didn't work out as-is because registering the callbacks post config change didn't work.
+ */
+public abstract class LoaderActivity<D> extends Activity implements
+        Loader.OnLoadCompleteListener<D> {
+    private boolean mStarted = false;
+
+    static final class LoaderInfo<D> {
+        public Bundle args;
+        public Loader<D> loader;
+    }
+    private HashMap<Integer, LoaderInfo<D>> mLoaders;
+    private HashMap<Integer, LoaderInfo<D>> mInactiveLoaders;
+
+    /**
+     * Registers a loader with this activity, registers the callbacks on it, and starts it loading.
+     * If a loader with the same id has previously been started it will automatically be destroyed
+     * when the new loader completes it's work. The callback will be delivered before the old loader
+     * is destroyed.
+     */
+    protected void startLoading(int id, Bundle args) {
+        LoaderInfo<D> info = mLoaders.get(id);
+        if (info != null) {
+            // Keep track of the previous instance of this loader so we can destroy
+            // it when the new one completes.
+            mInactiveLoaders.put(id, info);
+        }
+        info = new LoaderInfo<D>();
+        info.args = args;
+        mLoaders.put(id, info);
+        Loader<D> loader = onCreateLoader(id, args);
+        info.loader = loader;
+        if (mStarted) {
+            // The activity will start all existing loaders in it's onStart(), so only start them
+            // here if we're past that point of the activitiy's life cycle
+            loader.registerListener(id, this);
+            loader.startLoading();
+        }
+    }
+
+    protected abstract Loader<D> onCreateLoader(int id, Bundle args);
+    protected abstract void onInitializeLoaders();
+    protected abstract void onLoadFinished(Loader<D> loader, D data);
+
+    public final void onLoadComplete(Loader<D> loader, D data) {
+        // Notify of the new data so the app can switch out the old data before
+        // we try to destroy it.
+        onLoadFinished(loader, data);
+
+        // Look for an inactive loader and destroy it if found
+        int id = loader.getId();
+        LoaderInfo<D> info = mInactiveLoaders.get(id);
+        if (info != null) {
+            Loader<D> oldLoader = info.loader;
+            if (oldLoader != null) {
+                oldLoader.destroy();
+            }
+            mInactiveLoaders.remove(id);
+        }
+    }
+
+    @Override
+    public void onCreate(Bundle savedState) {
+        super.onCreate(savedState);
+
+        if (mLoaders == null) {
+            // Look for a passed along loader and create a new one if it's not there
+            mLoaders = (HashMap<Integer, LoaderInfo<D>>) getLastNonConfigurationInstance();
+            if (mLoaders == null) {
+                mLoaders = new HashMap<Integer, LoaderInfo<D>>();
+                onInitializeLoaders();
+            }
+        }
+        if (mInactiveLoaders == null) {
+            mInactiveLoaders = new HashMap<Integer, LoaderInfo<D>>();
+        }
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+
+        // Call out to sub classes so they can start their loaders
+        // Let the existing loaders know that we want to be notified when a load is complete
+        for (HashMap.Entry<Integer, LoaderInfo<D>> entry : mLoaders.entrySet()) {
+            LoaderInfo<D> info = entry.getValue();
+            Loader<D> loader = info.loader;
+            int id = entry.getKey();
+            if (loader == null) {
+               loader = onCreateLoader(id, info.args);
+               info.loader = loader;
+            }
+            loader.registerListener(id, this);
+            loader.startLoading();
+        }
+
+        mStarted = true;
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+
+        for (HashMap.Entry<Integer, LoaderInfo<D>> entry : mLoaders.entrySet()) {
+            LoaderInfo<D> info = entry.getValue();
+            Loader<D> loader = info.loader;
+            if (loader == null) {
+                continue;
+            }
+
+            // Let the loader know we're done with it
+            loader.unregisterListener(this);
+
+            // The loader isn't getting passed along to the next instance so ask it to stop loading
+//            if (!isChangingConfigurations()) {
+//                loader.stopLoading();
+//            }
+        }
+
+        mStarted = false;
+    }
+
+    @Override
+    public Object onRetainNonConfigurationInstance() {
+        // Pass the loader along to the next guy
+        Object result = mLoaders;
+        mLoaders = null;
+        return result;
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+
+        if (mLoaders != null) {
+            for (HashMap.Entry<Integer, LoaderInfo<D>> entry : mLoaders.entrySet()) {
+                LoaderInfo<D> info = entry.getValue();
+                Loader<D> loader = info.loader;
+                if (loader == null) {
+                    continue;
+                }
+                loader.destroy();
+            }
+        }
+    }
+}
diff --git a/src/android/app/patterns/LoaderManagingFragment.java b/src/android/app/patterns/LoaderManagingFragment.java
new file mode 100644
index 0000000..a337b1f
--- /dev/null
+++ b/src/android/app/patterns/LoaderManagingFragment.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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 android.app.patterns;
+
+import android.app.Fragment;
+import android.os.Bundle;
+
+import java.util.HashMap;
+
+public abstract class LoaderManagingFragment<D> extends Fragment
+        implements Loader.OnLoadCompleteListener<D> {
+    private boolean mStarted = false;
+
+    static final class LoaderInfo<D> {
+        public Bundle args;
+        public Loader<D> loader;
+    }
+    private HashMap<Integer, LoaderInfo<D>> mLoaders;
+    private HashMap<Integer, LoaderInfo<D>> mInactiveLoaders;
+
+    /**
+     * Registers a loader with this activity, registers the callbacks on it, and starts it loading.
+     * If a loader with the same id has previously been started it will automatically be destroyed
+     * when the new loader completes it's work. The callback will be delivered before the old loader
+     * is destroyed.
+     */
+    protected Loader<D> startLoading(int id, Bundle args) {
+        LoaderInfo<D> info = mLoaders.get(id);
+        if (info != null) {
+            // Keep track of the previous instance of this loader so we can destroy
+            // it when the new one completes.
+            mInactiveLoaders.put(id, info);
+        }
+        info = new LoaderInfo<D>();
+        info.args = args;
+        mLoaders.put(id, info);
+        Loader<D> loader = onCreateLoader(id, args);
+        info.loader = loader;
+        if (mStarted) {
+            // The activity will start all existing loaders in it's onStart(), so only start them
+            // here if we're past that point of the activitiy's life cycle
+            loader.registerListener(id, this);
+            loader.startLoading();
+        }
+        return loader;
+    }
+
+    protected abstract Loader<D> onCreateLoader(int id, Bundle args);
+    protected abstract void onInitializeLoaders();
+    protected abstract void onLoadFinished(Loader<D> loader, D data);
+
+    public final void onLoadComplete(Loader<D> loader, D data) {
+        // Notify of the new data so the app can switch out the old data before
+        // we try to destroy it.
+        onLoadFinished(loader, data);
+
+        // Look for an inactive loader and destroy it if found
+        int id = loader.getId();
+        LoaderInfo<D> info = mInactiveLoaders.get(id);
+        if (info != null) {
+            Loader<D> oldLoader = info.loader;
+            if (oldLoader != null) {
+                oldLoader.destroy();
+            }
+            mInactiveLoaders.remove(id);
+        }
+    }
+
+    @Override
+    public void onCreate(Bundle savedState) {
+        super.onCreate(savedState);
+
+        if (mLoaders == null) {
+            // Look for a passed along loader and create a new one if it's not there
+// TODO: uncomment once getLastNonConfigurationInstance method is available
+//            mLoaders = (HashMap<Integer, LoaderInfo>) getLastNonConfigurationInstance();
+            if (mLoaders == null) {
+                mLoaders = new HashMap<Integer, LoaderInfo<D>>();
+                onInitializeLoaders();
+            }
+        }
+        if (mInactiveLoaders == null) {
+            mInactiveLoaders = new HashMap<Integer, LoaderInfo<D>>();
+        }
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+
+        // Call out to sub classes so they can start their loaders
+        // Let the existing loaders know that we want to be notified when a load is complete
+        for (HashMap.Entry<Integer, LoaderInfo<D>> entry : mLoaders.entrySet()) {
+            LoaderInfo<D> info = entry.getValue();
+            Loader<D> loader = info.loader;
+            int id = entry.getKey();
+            if (loader == null) {
+               loader = onCreateLoader(id, info.args);
+               info.loader = loader;
+            }
+            loader.registerListener(id, this);
+            loader.startLoading();
+        }
+
+        mStarted = true;
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+
+        for (HashMap.Entry<Integer, LoaderInfo<D>> entry : mLoaders.entrySet()) {
+            LoaderInfo<D> info = entry.getValue();
+            Loader<D> loader = info.loader;
+            if (loader == null) {
+                continue;
+            }
+
+            // Let the loader know we're done with it
+            loader.unregisterListener(this);
+
+            // The loader isn't getting passed along to the next instance so ask it to stop loading
+// TODO: uncomment once isChangingConfig method is available
+//            if (!getActivity().isChangingConfigurations()) {
+                loader.stopLoading();
+//            }
+        }
+
+        mStarted = false;
+    }
+
+    /** TO DO: This needs to be turned into a retained fragment.
+    @Override
+    public Object onRetainNonConfigurationInstance() {
+        // Pass the loader along to the next guy
+        Object result = mLoaders;
+        mLoaders = null;
+        return result;
+    }
+    **/
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+
+        if (mLoaders != null) {
+            for (HashMap.Entry<Integer, LoaderInfo<D>> entry : mLoaders.entrySet()) {
+                LoaderInfo<D> info = entry.getValue();
+                Loader<D> loader = info.loader;
+                if (loader == null) {
+                    continue;
+                }
+                loader.destroy();
+            }
+        }
+    }
+}
diff --git a/src/com/android/contacts/CallContactActivity.java b/src/com/android/contacts/CallContactActivity.java
new file mode 100644
index 0000000..181e9b7
--- /dev/null
+++ b/src/com/android/contacts/CallContactActivity.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts;
+
+import com.android.contacts.list.CallOrSmsInitiator;
+
+import android.app.Activity;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.DialogInterface.OnDismissListener;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract.Contacts;
+
+/**
+ * An interstitial activity used when the user selects a QSB search suggestion using
+ * a call button.
+ */
+public class CallContactActivity extends Activity implements OnDismissListener {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        Uri data = getIntent().getData();
+        if (data == null) {
+            finish();
+        }
+
+        if (Contacts.CONTENT_ITEM_TYPE.equals(getContentResolver().getType(data))) {
+            CallOrSmsInitiator initiator = new CallOrSmsInitiator(this);
+            initiator.setOnDismissListener(this);
+            initiator.initiateCall(data);
+        } else {
+            startActivity(new Intent(Intent.ACTION_CALL_PRIVILEGED, data));
+            finish();
+        }
+    }
+
+    public void onDismiss(DialogInterface dialog) {
+        finish();
+    }
+}
diff --git a/src/com/android/contacts/ContactEntryAdapter.java b/src/com/android/contacts/ContactEntryAdapter.java
index 34ee505..90a41ca 100644
--- a/src/com/android/contacts/ContactEntryAdapter.java
+++ b/src/com/android/contacts/ContactEntryAdapter.java
@@ -77,7 +77,8 @@
         }
     }
 
-    ContactEntryAdapter(Context context, ArrayList<ArrayList<E>> sections, boolean separators) {
+    protected ContactEntryAdapter(Context context, ArrayList<ArrayList<E>> sections,
+            boolean separators) {
         mContext = context;
         mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
         mSections = sections;
diff --git a/src/com/android/contacts/ContactEntryListView.java b/src/com/android/contacts/ContactEntryListView.java
new file mode 100644
index 0000000..795be43
--- /dev/null
+++ b/src/com/android/contacts/ContactEntryListView.java
@@ -0,0 +1,85 @@
+/*
+ * 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.list.ContactEntryListAdapter;
+import com.android.contacts.widget.PinnedHeaderListView;
+import com.android.contacts.widget.TextHighlightingAnimation;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.AbsListView;
+import android.widget.ListAdapter;
+
+/**
+ * A custom list view for a list of contacts or contact-related entries.  It handles
+ * animation of names on scroll.
+ */
+public class ContactEntryListView extends PinnedHeaderListView {
+
+    private static final int TEXT_HIGHLIGHTING_ANIMATION_DURATION = 350;
+
+    private final TextHighlightingAnimation mHighlightingAnimation =
+            new ContactNameHighlightingAnimation(this, TEXT_HIGHLIGHTING_ANIMATION_DURATION);
+
+    private boolean mHighlightNamesWhenScrolling;
+
+    public ContactEntryListView(Context context) {
+        super(context);
+    }
+
+    public ContactEntryListView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public ContactEntryListView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    public TextHighlightingAnimation getTextHighlightingAnimation() {
+        return mHighlightingAnimation;
+    }
+
+    public boolean getHighlightNamesWhenScrolling() {
+        return mHighlightNamesWhenScrolling;
+    }
+
+    public void setHighlightNamesWhenScrolling(boolean flag) {
+        mHighlightNamesWhenScrolling = flag;
+    }
+
+    @Override
+    public void setAdapter(ListAdapter adapter) {
+        super.setAdapter(adapter);
+        if (adapter instanceof ContactEntryListAdapter) {
+            ((ContactEntryListAdapter)adapter)
+                    .setTextWithHighlightingFactory(mHighlightingAnimation);
+        }
+    }
+
+    @Override
+    public void onScrollStateChanged(AbsListView view, int scrollState) {
+        super.onScrollStateChanged(view, scrollState);
+        if (mHighlightNamesWhenScrolling) {
+            if (scrollState != OnScrollListener.SCROLL_STATE_IDLE) {
+                mHighlightingAnimation.startHighlighting();
+            } else {
+                mHighlightingAnimation.stopHighlighting();
+            }
+        }
+    }
+}
diff --git a/src/com/android/contacts/ContactListEmptyView.java b/src/com/android/contacts/ContactListEmptyView.java
new file mode 100644
index 0000000..40d5152
--- /dev/null
+++ b/src/com/android/contacts/ContactListEmptyView.java
@@ -0,0 +1,113 @@
+/*
+ * 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 android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.IContentService;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.telephony.TelephonyManager;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+/**
+ * Displays a message when there is nothing to display in a contact list.
+ */
+public class ContactListEmptyView extends ScrollView {
+
+    private static final String TAG = "ContactListEmptyView";
+
+    public ContactListEmptyView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public void hide() {
+        TextView empty = (TextView) findViewById(R.id.emptyText);
+        empty.setVisibility(GONE);
+    }
+
+    public void show(boolean searchMode, boolean displayOnlyPhones,
+            boolean isFavoritesMode, boolean isQueryMode, boolean isShortcutAction,
+            boolean isMultipleSelectionEnabled, boolean showSelectedOnly) {
+        if (searchMode) {
+            return;
+        }
+
+        TextView empty = (TextView) findViewById(R.id.emptyText);
+        Context context = getContext();
+        if (displayOnlyPhones) {
+            empty.setText(context.getText(R.string.noContactsWithPhoneNumbers));
+        } else if (isFavoritesMode) {
+            empty.setText(context.getText(R.string.noFavoritesHelpText));
+        } else if (isQueryMode) {
+            empty.setText(context.getText(R.string.noMatchingContacts));
+        } if (isMultipleSelectionEnabled) {
+            if (showSelectedOnly) {
+                empty.setText(context.getText(R.string.no_contacts_selected));
+            } else {
+                empty.setText(context.getText(R.string.noContactsWithPhoneNumbers));
+            }
+        } else {
+            TelephonyManager telephonyManager =
+                    (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+            boolean hasSim = telephonyManager.hasIccCard();
+            if (isSyncActive()) {
+                if (isShortcutAction) {
+                    // Help text is the same no matter whether there is SIM or not.
+                    empty.setText(
+                            context.getText(R.string.noContactsHelpTextWithSyncForCreateShortcut));
+                } else if (hasSim) {
+                    empty.setText(context.getText(R.string.noContactsHelpTextWithSync));
+                } else {
+                    empty.setText(context.getText(R.string.noContactsNoSimHelpTextWithSync));
+                }
+            } else {
+                if (isShortcutAction) {
+                    // Help text is the same no matter whether there is SIM or not.
+                    empty.setText(context.getText(R.string.noContactsHelpTextForCreateShortcut));
+                } else if (hasSim) {
+                    empty.setText(context.getText(R.string.noContactsHelpText));
+                } else {
+                    empty.setText(context.getText(R.string.noContactsNoSimHelpText));
+                }
+            }
+        }
+        empty.setVisibility(VISIBLE);
+    }
+
+    private boolean isSyncActive() {
+        Account[] accounts = AccountManager.get(getContext()).getAccounts();
+        if (accounts != null && accounts.length > 0) {
+            IContentService contentService = ContentResolver.getContentService();
+            for (Account account : accounts) {
+                try {
+                    if (contentService.isSyncActive(account, ContactsContract.AUTHORITY)) {
+                        return true;
+                    }
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Could not get the sync status");
+                }
+            }
+        }
+        return false;
+    }
+}
diff --git a/src/com/android/contacts/ContactNameHighlightingAnimation.java b/src/com/android/contacts/ContactNameHighlightingAnimation.java
new file mode 100644
index 0000000..68664b3
--- /dev/null
+++ b/src/com/android/contacts/ContactNameHighlightingAnimation.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2007 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.list.ContactListItemView;
+import com.android.contacts.widget.TextHighlightingAnimation;
+
+import android.view.View;
+import android.widget.ListView;
+
+/**
+ * A {@link TextHighlightingAnimation} that redraws just the contact display name in a
+ * list item.
+ */
+public class ContactNameHighlightingAnimation extends TextHighlightingAnimation {
+    private final ListView mListView;
+
+    public ContactNameHighlightingAnimation(ListView listView, int duration) {
+        super(duration);
+        this.mListView = listView;
+    }
+
+    /**
+     * Redraws all visible items of the list corresponding to contacts
+     */
+    @Override
+    protected void invalidate() {
+        int childCount = mListView.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View itemView = mListView.getChildAt(i);
+            if (itemView instanceof ContactListItemView) {
+                final ContactListItemView view = (ContactListItemView)itemView;
+                view.getNameTextView().invalidate();
+            }
+        }
+    }
+
+    @Override
+    protected void onAnimationStarted() {
+        mListView.setScrollingCacheEnabled(false);
+    }
+
+    @Override
+    protected void onAnimationEnded() {
+        mListView.setScrollingCacheEnabled(true);
+    }
+}
diff --git a/src/com/android/contacts/ContactsListActivity.java b/src/com/android/contacts/ContactsListActivity.java
index 0d2c7eb..bfb6a96 100644
--- a/src/com/android/contacts/ContactsListActivity.java
+++ b/src/com/android/contacts/ContactsListActivity.java
@@ -16,475 +16,94 @@
 
 package com.android.contacts;
 
-import com.android.contacts.TextHighlightingAnimation.TextWithHighlighting;
+import com.android.contacts.list.CallOrSmsInitiator;
+import com.android.contacts.list.ContactBrowseListContextMenuAdapter;
+import com.android.contacts.list.ContactEntryListFragment;
+import com.android.contacts.list.ContactPickerFragment;
+import com.android.contacts.list.ContactsIntentResolver;
+import com.android.contacts.list.ContactsRequest;
+import com.android.contacts.list.DefaultContactBrowseListFragment;
+import com.android.contacts.list.OnContactBrowserActionListener;
+import com.android.contacts.list.OnContactPickerActionListener;
+import com.android.contacts.list.OnPhoneNumberPickerActionListener;
+import com.android.contacts.list.OnPostalAddressPickerActionListener;
+import com.android.contacts.list.PhoneNumberPickerFragment;
+import com.android.contacts.list.PostalAddressPickerFragment;
+import com.android.contacts.list.StrequentContactListFragment;
 import com.android.contacts.model.ContactsSource;
 import com.android.contacts.model.Sources;
-import com.android.contacts.ui.ContactsPreferences;
 import com.android.contacts.ui.ContactsPreferencesActivity;
-import com.android.contacts.ui.ContactsPreferencesActivity.Prefs;
 import com.android.contacts.util.AccountSelectionUtil;
-import com.android.contacts.util.Constants;
+import com.android.contacts.widget.ContextMenuAdapter;
+import com.android.contacts.widget.SearchEditText;
+import com.android.contacts.widget.SearchEditText.OnFilterTextListener;
 
 import android.accounts.Account;
-import android.accounts.AccountManager;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.Dialog;
-import android.app.ListActivity;
+import android.app.FragmentTransaction;
 import android.app.SearchManager;
-import android.content.AsyncQueryHandler;
-import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.DialogInterface;
-import android.content.IContentService;
 import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.UriMatcher;
-import android.content.res.ColorStateList;
 import android.content.res.Resources;
-import android.database.CharArrayBuffer;
-import android.database.ContentObserver;
 import android.database.Cursor;
-import android.database.MatrixCursor;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.graphics.Typeface;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
 import android.net.Uri;
-import android.net.Uri.Builder;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Parcelable;
-import android.os.RemoteException;
-import android.preference.PreferenceManager;
 import android.provider.ContactsContract;
 import android.provider.Settings;
-import android.provider.Contacts.ContactMethods;
-import android.provider.Contacts.People;
-import android.provider.Contacts.PeopleColumns;
-import android.provider.Contacts.Phones;
-import android.provider.ContactsContract.ContactCounts;
 import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.Intents;
-import android.provider.ContactsContract.ProviderStatus;
 import android.provider.ContactsContract.RawContacts;
-import android.provider.ContactsContract.SearchSnippetColumns;
-import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.Nickname;
-import android.provider.ContactsContract.CommonDataKinds.Organization;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.Photo;
-import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
-import android.provider.ContactsContract.Contacts.AggregationSuggestions;
-import android.provider.ContactsContract.Intents.Insert;
-import android.provider.ContactsContract.Intents.UI;
 import android.telephony.TelephonyManager;
-import android.text.Editable;
-import android.text.Html;
 import android.text.TextUtils;
-import android.text.TextWatcher;
 import android.util.Log;
-import android.view.ContextMenu;
 import android.view.ContextThemeWrapper;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
-import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ContextMenu.ContextMenuInfo;
-import android.view.View.OnClickListener;
-import android.view.View.OnFocusChangeListener;
-import android.view.View.OnTouchListener;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.AbsListView;
-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.SectionIndexer;
 import android.widget.TextView;
 import android.widget.Toast;
-import android.widget.AbsListView.OnScrollListener;
 
-import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Random;
 
 /**
  * Displays a list of contacts. Usually is embedded into the ContactsActivity.
  */
 @SuppressWarnings("deprecation")
-public class ContactsListActivity extends ListActivity implements View.OnCreateContextMenuListener,
-        View.OnClickListener, View.OnKeyListener, TextWatcher, TextView.OnEditorActionListener,
-        OnFocusChangeListener, OnTouchListener {
-
-    public static class JoinContactActivity extends ContactsListActivity {
-
-    }
-
-    public static class ContactsSearchActivity extends ContactsListActivity {
-
-    }
+public class ContactsListActivity extends Activity implements View.OnCreateContextMenuListener {
 
     private static final String TAG = "ContactsListActivity";
 
-    private static final boolean ENABLE_ACTION_ICON_OVERLAYS = true;
-
-    private static final String LIST_STATE_KEY = "liststate";
-    private static final String SHORTCUT_ACTION_KEY = "shortcutAction";
-
-    static final int MENU_ITEM_VIEW_CONTACT = 1;
-    static final int MENU_ITEM_CALL = 2;
-    static final int MENU_ITEM_EDIT_BEFORE_CALL = 3;
-    static final int MENU_ITEM_SEND_SMS = 4;
-    static final int MENU_ITEM_SEND_IM = 5;
-    static final int MENU_ITEM_EDIT = 6;
-    static final int MENU_ITEM_DELETE = 7;
-    static final int MENU_ITEM_TOGGLE_STAR = 8;
-
     private static final int SUBACTIVITY_NEW_CONTACT = 1;
     private static final int SUBACTIVITY_VIEW_CONTACT = 2;
     private static final int SUBACTIVITY_DISPLAY_GROUP = 3;
     private static final int SUBACTIVITY_SEARCH = 4;
-    private static final int SUBACTIVITY_FILTER = 5;
+    protected static final int SUBACTIVITY_FILTER = 5;
 
-    private static final int TEXT_HIGHLIGHTING_ANIMATION_DURATION = 350;
-
-    /**
-     * The action for the join contact activity.
-     * <p>
-     * Input: extra field {@link #EXTRA_AGGREGATE_ID} is the aggregate ID.
-     *
-     * TODO: move to {@link ContactsContract}.
-     */
-    public static final String JOIN_AGGREGATE =
-            "com.android.contacts.action.JOIN_AGGREGATE";
-
-    /**
-     * Used with {@link #JOIN_AGGREGATE} to give it the target for aggregation.
-     * <p>
-     * Type: LONG
-     */
-    public static final String EXTRA_AGGREGATE_ID =
-            "com.android.contacts.action.AGGREGATE_ID";
-
-    /**
-     * Used with {@link #JOIN_AGGREGATE} to give it the name of the aggregation target.
-     * <p>
-     * Type: STRING
-     */
-    @Deprecated
-    public static final String EXTRA_AGGREGATE_NAME =
-            "com.android.contacts.action.AGGREGATE_NAME";
-
-    public static final String AUTHORITIES_FILTER_KEY = "authorities";
-
-    private static final Uri CONTACTS_CONTENT_URI_WITH_LETTER_COUNTS =
-            buildSectionIndexerUri(Contacts.CONTENT_URI);
-
-    /** Mask for picker mode */
-    static final int MODE_MASK_PICKER = 0x80000000;
-    /** Mask for no presence mode */
-    static final int MODE_MASK_NO_PRESENCE = 0x40000000;
-    /** Mask for enabling list filtering */
-    static final int MODE_MASK_NO_FILTER = 0x20000000;
-    /** Mask for having a "create new contact" header in the list */
-    static final int MODE_MASK_CREATE_NEW = 0x10000000;
-    /** Mask for showing photos in the list */
-    static final int MODE_MASK_SHOW_PHOTOS = 0x08000000;
-    /** Mask for hiding additional information e.g. primary phone number in the list */
-    static final int MODE_MASK_NO_DATA = 0x04000000;
-    /** Mask for showing a call button in the list */
-    static final int MODE_MASK_SHOW_CALL_BUTTON = 0x02000000;
-    /** Mask to disable quickcontact (images will show as normal images) */
-    static final int MODE_MASK_DISABLE_QUIKCCONTACT = 0x01000000;
-    /** Mask to show the total number of contacts at the top */
-    static final int MODE_MASK_SHOW_NUMBER_OF_CONTACTS = 0x00800000;
-
-    /** Unknown mode */
-    static final int MODE_UNKNOWN = 0;
-    /** Default mode */
-    static final int MODE_DEFAULT = 4 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_SHOW_NUMBER_OF_CONTACTS;
-    /** Custom mode */
-    static final int MODE_CUSTOM = 8;
-    /** Show all starred contacts */
-    static final int MODE_STARRED = 20 | MODE_MASK_SHOW_PHOTOS;
-    /** Show frequently contacted contacts */
-    static final int MODE_FREQUENT = 30 | MODE_MASK_SHOW_PHOTOS;
-    /** Show starred and the frequent */
-    static final int MODE_STREQUENT = 35 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_SHOW_CALL_BUTTON;
-    /** Show all contacts and pick them when clicking */
-    static final int MODE_PICK_CONTACT = 40 | MODE_MASK_PICKER | MODE_MASK_SHOW_PHOTOS
-            | MODE_MASK_DISABLE_QUIKCCONTACT;
-    /** Show all contacts as well as the option to create a new one */
-    static final int MODE_PICK_OR_CREATE_CONTACT = 42 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW
-            | MODE_MASK_SHOW_PHOTOS | MODE_MASK_DISABLE_QUIKCCONTACT;
-    /** Show all people through the legacy provider and pick them when clicking */
-    static final int MODE_LEGACY_PICK_PERSON = 43 | MODE_MASK_PICKER
-            | MODE_MASK_DISABLE_QUIKCCONTACT;
-    /** Show all people through the legacy provider as well as the option to create a new one */
-    static final int MODE_LEGACY_PICK_OR_CREATE_PERSON = 44 | MODE_MASK_PICKER
-            | MODE_MASK_CREATE_NEW | MODE_MASK_DISABLE_QUIKCCONTACT;
-    /** Show all contacts and pick them when clicking, and allow creating a new contact */
-    static final int MODE_INSERT_OR_EDIT_CONTACT = 45 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW
-            | MODE_MASK_SHOW_PHOTOS | MODE_MASK_DISABLE_QUIKCCONTACT;
-    /** Show all phone numbers and pick them when clicking */
-    static final int MODE_PICK_PHONE = 50 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE;
-    /** Show all phone numbers through the legacy provider and pick them when clicking */
-    static final int MODE_LEGACY_PICK_PHONE =
-            51 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER;
-    /** Show all postal addresses and pick them when clicking */
-    static final int MODE_PICK_POSTAL =
-            55 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER;
-    /** Show all postal addresses and pick them when clicking */
-    static final int MODE_LEGACY_PICK_POSTAL =
-            56 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER;
-    static final int MODE_GROUP = 57 | MODE_MASK_SHOW_PHOTOS;
-    /** Run a search query */
-    static final int MODE_QUERY = 60 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_NO_FILTER
-            | MODE_MASK_SHOW_NUMBER_OF_CONTACTS;
-    /** Run a search query in PICK mode, but that still launches to VIEW */
-    static final int MODE_QUERY_PICK_TO_VIEW = 65 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_PICKER
-            | MODE_MASK_SHOW_NUMBER_OF_CONTACTS;
-
-    /** Show join suggestions followed by an A-Z list */
-    static final int MODE_JOIN_CONTACT = 70 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE
-            | MODE_MASK_NO_DATA | MODE_MASK_SHOW_PHOTOS | MODE_MASK_DISABLE_QUIKCCONTACT;
-
-    /** Run a search query in a PICK mode */
-    static final int MODE_QUERY_PICK = 75 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_NO_FILTER
-            | MODE_MASK_PICKER | MODE_MASK_DISABLE_QUIKCCONTACT | MODE_MASK_SHOW_NUMBER_OF_CONTACTS;
-
-    /** Run a search query in a PICK_PHONE mode */
-    static final int MODE_QUERY_PICK_PHONE = 80 | MODE_MASK_NO_FILTER | MODE_MASK_PICKER
-            | MODE_MASK_SHOW_NUMBER_OF_CONTACTS;
-
-    /** Run a search query in PICK mode, but that still launches to EDIT */
-    static final int MODE_QUERY_PICK_TO_EDIT = 85 | MODE_MASK_NO_FILTER | MODE_MASK_SHOW_PHOTOS
-            | MODE_MASK_PICKER | MODE_MASK_SHOW_NUMBER_OF_CONTACTS;
-
-    /**
-     * An action used to do perform search while in a contact picker.  It is initiated
-     * by the ContactListActivity itself.
-     */
-    private static final String ACTION_SEARCH_INTERNAL = "com.android.contacts.INTERNAL_SEARCH";
-
-    /** Maximum number of suggestions shown for joining aggregates */
-    static final int MAX_SUGGESTIONS = 4;
-
-    static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] {
-        Contacts._ID,                       // 0
-        Contacts.DISPLAY_NAME_PRIMARY,      // 1
-        Contacts.DISPLAY_NAME_ALTERNATIVE,  // 2
-        Contacts.SORT_KEY_PRIMARY,          // 3
-        Contacts.STARRED,                   // 4
-        Contacts.TIMES_CONTACTED,           // 5
-        Contacts.CONTACT_PRESENCE,          // 6
-        Contacts.PHOTO_ID,                  // 7
-        Contacts.LOOKUP_KEY,                // 8
-        Contacts.PHONETIC_NAME,             // 9
-        Contacts.HAS_PHONE_NUMBER,          // 10
-    };
-    static final String[] CONTACTS_SUMMARY_PROJECTION_FROM_EMAIL = new String[] {
-        Contacts._ID,                       // 0
-        Contacts.DISPLAY_NAME_PRIMARY,      // 1
-        Contacts.DISPLAY_NAME_ALTERNATIVE,  // 2
-        Contacts.SORT_KEY_PRIMARY,          // 3
-        Contacts.STARRED,                   // 4
-        Contacts.TIMES_CONTACTED,           // 5
-        Contacts.CONTACT_PRESENCE,          // 6
-        Contacts.PHOTO_ID,                  // 7
-        Contacts.LOOKUP_KEY,                // 8
-        Contacts.PHONETIC_NAME,             // 9
-        // email lookup doesn't included HAS_PHONE_NUMBER in projection
-    };
-
-    static final String[] CONTACTS_SUMMARY_FILTER_PROJECTION = new String[] {
-        Contacts._ID,                       // 0
-        Contacts.DISPLAY_NAME_PRIMARY,      // 1
-        Contacts.DISPLAY_NAME_ALTERNATIVE,  // 2
-        Contacts.SORT_KEY_PRIMARY,          // 3
-        Contacts.STARRED,                   // 4
-        Contacts.TIMES_CONTACTED,           // 5
-        Contacts.CONTACT_PRESENCE,          // 6
-        Contacts.PHOTO_ID,                  // 7
-        Contacts.LOOKUP_KEY,                // 8
-        Contacts.PHONETIC_NAME,             // 9
-        Contacts.HAS_PHONE_NUMBER,          // 10
-        SearchSnippetColumns.SNIPPET_MIMETYPE, // 11
-        SearchSnippetColumns.SNIPPET_DATA1,     // 12
-        SearchSnippetColumns.SNIPPET_DATA4,     // 13
-    };
-
-    static final String[] LEGACY_PEOPLE_PROJECTION = new String[] {
-        People._ID,                         // 0
-        People.DISPLAY_NAME,                // 1
-        People.DISPLAY_NAME,                // 2
-        People.DISPLAY_NAME,                // 3
-        People.STARRED,                     // 4
-        PeopleColumns.TIMES_CONTACTED,      // 5
-        People.PRESENCE_STATUS,             // 6
-    };
-    static final int SUMMARY_ID_COLUMN_INDEX = 0;
-    static final int SUMMARY_DISPLAY_NAME_PRIMARY_COLUMN_INDEX = 1;
-    static final int SUMMARY_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX = 2;
-    static final int SUMMARY_SORT_KEY_PRIMARY_COLUMN_INDEX = 3;
-    static final int SUMMARY_STARRED_COLUMN_INDEX = 4;
-    static final int SUMMARY_TIMES_CONTACTED_COLUMN_INDEX = 5;
-    static final int SUMMARY_PRESENCE_STATUS_COLUMN_INDEX = 6;
-    static final int SUMMARY_PHOTO_ID_COLUMN_INDEX = 7;
-    static final int SUMMARY_LOOKUP_KEY_COLUMN_INDEX = 8;
-    static final int SUMMARY_PHONETIC_NAME_COLUMN_INDEX = 9;
-    static final int SUMMARY_HAS_PHONE_COLUMN_INDEX = 10;
-    static final int SUMMARY_SNIPPET_MIMETYPE_COLUMN_INDEX = 11;
-    static final int SUMMARY_SNIPPET_DATA1_COLUMN_INDEX = 12;
-    static final int SUMMARY_SNIPPET_DATA4_COLUMN_INDEX = 13;
-
-    static final String[] PHONES_PROJECTION = new String[] {
-        Phone._ID, //0
-        Phone.TYPE, //1
-        Phone.LABEL, //2
-        Phone.NUMBER, //3
-        Phone.DISPLAY_NAME, // 4
-        Phone.CONTACT_ID, // 5
-    };
-    static final String[] LEGACY_PHONES_PROJECTION = new String[] {
-        Phones._ID, //0
-        Phones.TYPE, //1
-        Phones.LABEL, //2
-        Phones.NUMBER, //3
-        People.DISPLAY_NAME, // 4
-    };
-    static final int PHONE_ID_COLUMN_INDEX = 0;
-    static final int PHONE_TYPE_COLUMN_INDEX = 1;
-    static final int PHONE_LABEL_COLUMN_INDEX = 2;
-    static final int PHONE_NUMBER_COLUMN_INDEX = 3;
-    static final int PHONE_DISPLAY_NAME_COLUMN_INDEX = 4;
-    static final int PHONE_CONTACT_ID_COLUMN_INDEX = 5;
-
-    static final String[] POSTALS_PROJECTION = new String[] {
-        StructuredPostal._ID, //0
-        StructuredPostal.TYPE, //1
-        StructuredPostal.LABEL, //2
-        StructuredPostal.DATA, //3
-        StructuredPostal.DISPLAY_NAME, // 4
-    };
-    static final String[] LEGACY_POSTALS_PROJECTION = new String[] {
-        ContactMethods._ID, //0
-        ContactMethods.TYPE, //1
-        ContactMethods.LABEL, //2
-        ContactMethods.DATA, //3
-        People.DISPLAY_NAME, // 4
-    };
-    static final String[] RAW_CONTACTS_PROJECTION = new String[] {
+    private static final String[] RAW_CONTACTS_PROJECTION = new String[] {
         RawContacts._ID, //0
         RawContacts.CONTACT_ID, //1
         RawContacts.ACCOUNT_TYPE, //2
     };
 
-    static final int POSTAL_ID_COLUMN_INDEX = 0;
-    static final int POSTAL_TYPE_COLUMN_INDEX = 1;
-    static final int POSTAL_LABEL_COLUMN_INDEX = 2;
-    static final int POSTAL_ADDRESS_COLUMN_INDEX = 3;
-    static final int POSTAL_DISPLAY_NAME_COLUMN_INDEX = 4;
-
-    private static final int QUERY_TOKEN = 42;
-
-    static final String KEY_PICKER_MODE = "picker_mode";
-
-    private ContactItemListAdapter mAdapter;
-
-    int mMode = MODE_DEFAULT;
-
-    private QueryHandler mQueryHandler;
-    private boolean mJustCreated;
-    private boolean mSyncEnabled;
-    Uri mSelectedContactUri;
-
-//    private boolean mDisplayAll;
-    private boolean mDisplayOnlyPhones;
-
-    private Uri mGroupUri;
-
-    private long mQueryAggregateId;
+    private Uri mSelectedContactUri;
 
     private ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>();
     private int  mWritableSourcesCnt;
     private int  mReadOnlySourcesCnt;
 
-    /**
-     * Used to keep track of the scroll state of the list.
-     */
-    private Parcelable mListState = null;
-
-    private String mShortcutAction;
-
-    /**
-     * Internal query type when in mode {@link #MODE_QUERY_PICK_TO_VIEW}.
-     */
-    private int mQueryMode = QUERY_MODE_NONE;
-
-    private static final int QUERY_MODE_NONE = -1;
-    private static final int QUERY_MODE_MAILTO = 1;
-    private static final int QUERY_MODE_TEL = 2;
-
-    private int mProviderStatus = ProviderStatus.STATUS_NORMAL;
-
-    private boolean mSearchMode;
-    private boolean mSearchResultsMode;
-    private boolean mShowNumberOfContacts;
-
-    private boolean mShowSearchSnippets;
-    private boolean mSearchInitiated;
-
-    private String mInitialFilter;
-
-    private static final String CLAUSE_ONLY_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1";
-    private static final String CLAUSE_ONLY_PHONES = Contacts.HAS_PHONE_NUMBER + "=1";
-
-    /**
-     * In the {@link #MODE_JOIN_CONTACT} determines whether we display a list item with the label
-     * "Show all contacts" or actually show all contacts
-     */
-    private boolean mJoinModeShowAllContacts;
-
-    /**
-     * The ID of the special item described above.
-     */
-    private static final long JOIN_MODE_SHOW_ALL_CONTACTS_ID = -2;
-
-    // Uri matcher for contact id
-    private static final int CONTACTS_ID = 1001;
-    private static final UriMatcher sContactsIdMatcher;
-
-    private ContactPhotoLoader mPhotoLoader;
-
-    final String[] sLookupProjection = new String[] {
+    private final String[] sLookupProjection = new String[] {
             Contacts.LOOKUP_KEY
     };
-
-    static {
-        sContactsIdMatcher = new UriMatcher(UriMatcher.NO_MATCH);
-        sContactsIdMatcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID);
-    }
-
     private class DeleteClickListener implements DialogInterface.OnClickListener {
         public void onClick(DialogInterface dialog, int which) {
             if (mSelectedContactUri != null) {
@@ -493,747 +112,359 @@
         }
     }
 
-    /**
-     * A {@link TextHighlightingAnimation} that redraws just the contact display name in a
-     * list item.
-     */
-    private static class NameHighlightingAnimation extends TextHighlightingAnimation {
-        private final ListView mListView;
+    private ContactsIntentResolver mIntentResolver;
+    protected ContactEntryListFragment<?> mListFragment;
 
-        private NameHighlightingAnimation(ListView listView, int duration) {
-            super(duration);
-            this.mListView = listView;
-        }
+    protected CallOrSmsInitiator mCallOrSmsInitiator;
 
-        /**
-         * Redraws all visible items of the list corresponding to contacts
-         */
-        @Override
-        protected void invalidate() {
-            int childCount = mListView.getChildCount();
-            for (int i = 0; i < childCount; i++) {
-                View itemView = mListView.getChildAt(i);
-                if (itemView instanceof ContactListItemView) {
-                    final ContactListItemView view = (ContactListItemView)itemView;
-                    view.getNameTextView().invalidate();
-                }
-            }
-        }
+    private int mActionCode;
 
-        @Override
-        protected void onAnimationStarted() {
-            mListView.setScrollingCacheEnabled(false);
-        }
+    private boolean mSearchInitiated;
 
-        @Override
-        protected void onAnimationEnded() {
-            mListView.setScrollingCacheEnabled(true);
-        }
-    }
-
-    // The size of a home screen shortcut icon.
-    private int mIconSize;
-    private ContactsPreferences mContactsPrefs;
-    private int mDisplayOrder;
-    private int mSortOrder;
-    private boolean mHighlightWhenScrolling;
-    private TextHighlightingAnimation mHighlightingAnimation;
+    private ContactsRequest mRequest;
     private SearchEditText mSearchEditText;
 
+    public ContactsListActivity() {
+        mIntentResolver = new ContactsIntentResolver(this);
+    }
+
     /**
-     * An approximation of the background color of the pinned header. This color
-     * is used when the pinned header is being pushed up.  At that point the header
-     * "fades away".  Rather than computing a faded bitmap based on the 9-patch
-     * normally used for the background, we will use a solid color, which will
-     * provide better performance and reduced complexity.
+     * Visible for testing: makes queries run on the UI thread.
      */
-    private int mPinnedHeaderBackgroundColor;
-
-    private ContentObserver mProviderStatusObserver = new ContentObserver(new Handler()) {
-
-        @Override
-        public void onChange(boolean selfChange) {
-            checkProviderState(true);
-        }
-    };
+    /* package */ void runQueriesSynchronously() {
+        // TODO
+    }
 
     @Override
     protected void onCreate(Bundle icicle) {
         super.onCreate(icicle);
 
-        mIconSize = getResources().getDimensionPixelSize(android.R.dimen.app_icon_size);
-        mContactsPrefs = new ContactsPreferences(this);
-        mPhotoLoader = new ContactPhotoLoader(this, R.drawable.ic_contact_list_picture);
-
-        // Resolve the intent
-        final Intent intent = getIntent();
-
-        // Allow the title to be set to a custom String using an extra on the intent
-        String title = intent.getStringExtra(UI.TITLE_EXTRA_KEY);
-        if (title != null) {
-            setTitle(title);
-        }
-
-        String action = intent.getAction();
-        String component = intent.getComponent().getClassName();
-
-        // When we get a FILTER_CONTACTS_ACTION, it represents search in the context
-        // of some other action. Let's retrieve the original action to provide proper
-        // context for the search queries.
-        if (UI.FILTER_CONTACTS_ACTION.equals(action)) {
-            mSearchMode = true;
-            mShowSearchSnippets = true;
-            Bundle extras = intent.getExtras();
-            if (extras != null) {
-                mInitialFilter = extras.getString(UI.FILTER_TEXT_EXTRA_KEY);
-                String originalAction =
-                        extras.getString(ContactsSearchManager.ORIGINAL_ACTION_EXTRA_KEY);
-                if (originalAction != null) {
-                    action = originalAction;
-                }
-                String originalComponent =
-                        extras.getString(ContactsSearchManager.ORIGINAL_COMPONENT_EXTRA_KEY);
-                if (originalComponent != null) {
-                    component = originalComponent;
-                }
-            } else {
-                mInitialFilter = null;
-            }
-        }
-
-        Log.i(TAG, "Called with action: " + action);
-        mMode = MODE_UNKNOWN;
-        if (UI.LIST_DEFAULT.equals(action) || UI.FILTER_CONTACTS_ACTION.equals(action)) {
-            mMode = MODE_DEFAULT;
-            // When mDefaultMode is true the mode is set in onResume(), since the preferneces
-            // activity may change it whenever this activity isn't running
-        } else if (UI.LIST_GROUP_ACTION.equals(action)) {
-            mMode = MODE_GROUP;
-            String groupName = intent.getStringExtra(UI.GROUP_NAME_EXTRA_KEY);
-            if (TextUtils.isEmpty(groupName)) {
-                finish();
-                return;
-            }
-            buildUserGroupUri(groupName);
-        } else if (UI.LIST_ALL_CONTACTS_ACTION.equals(action)) {
-            mMode = MODE_CUSTOM;
-            mDisplayOnlyPhones = false;
-        } else if (UI.LIST_STARRED_ACTION.equals(action)) {
-            mMode = mSearchMode ? MODE_DEFAULT : MODE_STARRED;
-        } else if (UI.LIST_FREQUENT_ACTION.equals(action)) {
-            mMode = mSearchMode ? MODE_DEFAULT : MODE_FREQUENT;
-        } else if (UI.LIST_STREQUENT_ACTION.equals(action)) {
-            mMode = mSearchMode ? MODE_DEFAULT : MODE_STREQUENT;
-        } else if (UI.LIST_CONTACTS_WITH_PHONES_ACTION.equals(action)) {
-            mMode = MODE_CUSTOM;
-            mDisplayOnlyPhones = true;
-        } else if (Intent.ACTION_PICK.equals(action)) {
-            // XXX These should be showing the data from the URI given in
-            // the Intent.
-            final String type = intent.resolveType(this);
-            if (Contacts.CONTENT_TYPE.equals(type)) {
-                mMode = MODE_PICK_CONTACT;
-            } else if (People.CONTENT_TYPE.equals(type)) {
-                mMode = MODE_LEGACY_PICK_PERSON;
-            } else if (Phone.CONTENT_TYPE.equals(type)) {
-                mMode = MODE_PICK_PHONE;
-            } else if (Phones.CONTENT_TYPE.equals(type)) {
-                mMode = MODE_LEGACY_PICK_PHONE;
-            } else if (StructuredPostal.CONTENT_TYPE.equals(type)) {
-                mMode = MODE_PICK_POSTAL;
-            } else if (ContactMethods.CONTENT_POSTAL_TYPE.equals(type)) {
-                mMode = MODE_LEGACY_PICK_POSTAL;
-            }
-        } else if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) {
-            if (component.equals("alias.DialShortcut")) {
-                mMode = MODE_PICK_PHONE;
-                mShortcutAction = Intent.ACTION_CALL;
-                mShowSearchSnippets = false;
-                setTitle(R.string.callShortcutActivityTitle);
-            } else if (component.equals("alias.MessageShortcut")) {
-                mMode = MODE_PICK_PHONE;
-                mShortcutAction = Intent.ACTION_SENDTO;
-                mShowSearchSnippets = false;
-                setTitle(R.string.messageShortcutActivityTitle);
-            } else if (mSearchMode) {
-                mMode = MODE_PICK_CONTACT;
-                mShortcutAction = Intent.ACTION_VIEW;
-                setTitle(R.string.shortcutActivityTitle);
-            } else {
-                mMode = MODE_PICK_OR_CREATE_CONTACT;
-                mShortcutAction = Intent.ACTION_VIEW;
-                setTitle(R.string.shortcutActivityTitle);
-            }
-        } else if (Intent.ACTION_GET_CONTENT.equals(action)) {
-            final String type = intent.resolveType(this);
-            if (Contacts.CONTENT_ITEM_TYPE.equals(type)) {
-                if (mSearchMode) {
-                    mMode = MODE_PICK_CONTACT;
-                } else {
-                    mMode = MODE_PICK_OR_CREATE_CONTACT;
-                }
-            } else if (Phone.CONTENT_ITEM_TYPE.equals(type)) {
-                mMode = MODE_PICK_PHONE;
-            } else if (Phones.CONTENT_ITEM_TYPE.equals(type)) {
-                mMode = MODE_LEGACY_PICK_PHONE;
-            } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(type)) {
-                mMode = MODE_PICK_POSTAL;
-            } else if (ContactMethods.CONTENT_POSTAL_ITEM_TYPE.equals(type)) {
-                mMode = MODE_LEGACY_PICK_POSTAL;
-            }  else if (People.CONTENT_ITEM_TYPE.equals(type)) {
-                if (mSearchMode) {
-                    mMode = MODE_LEGACY_PICK_PERSON;
-                } else {
-                    mMode = MODE_LEGACY_PICK_OR_CREATE_PERSON;
-                }
-            }
-
-        } else if (Intent.ACTION_INSERT_OR_EDIT.equals(action)) {
-            mMode = MODE_INSERT_OR_EDIT_CONTACT;
-        } else if (Intent.ACTION_SEARCH.equals(action)) {
-            // See if the suggestion was clicked with a search action key (call button)
-            if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG))) {
-                String query = intent.getStringExtra(SearchManager.QUERY);
-                if (!TextUtils.isEmpty(query)) {
-                    Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
-                            Uri.fromParts("tel", query, null));
-                    startActivity(newIntent);
-                }
-                finish();
-                return;
-            }
-
-            // See if search request has extras to specify query
-            if (intent.hasExtra(Insert.EMAIL)) {
-                mMode = MODE_QUERY_PICK_TO_VIEW;
-                mQueryMode = QUERY_MODE_MAILTO;
-                mInitialFilter = intent.getStringExtra(Insert.EMAIL);
-            } else if (intent.hasExtra(Insert.PHONE)) {
-                mMode = MODE_QUERY_PICK_TO_VIEW;
-                mQueryMode = QUERY_MODE_TEL;
-                mInitialFilter = intent.getStringExtra(Insert.PHONE);
-            } else {
-                // Otherwise handle the more normal search case
-                mMode = MODE_QUERY;
-                mShowSearchSnippets = true;
-                mInitialFilter = getIntent().getStringExtra(SearchManager.QUERY);
-            }
-            mSearchResultsMode = true;
-        } else if (ACTION_SEARCH_INTERNAL.equals(action)) {
-            String originalAction = null;
-            Bundle extras = intent.getExtras();
-            if (extras != null) {
-                originalAction = extras.getString(ContactsSearchManager.ORIGINAL_ACTION_EXTRA_KEY);
-            }
-            mShortcutAction = intent.getStringExtra(SHORTCUT_ACTION_KEY);
-
-            if (Intent.ACTION_INSERT_OR_EDIT.equals(originalAction)) {
-                mMode = MODE_QUERY_PICK_TO_EDIT;
-                mShowSearchSnippets = true;
-                mInitialFilter = getIntent().getStringExtra(SearchManager.QUERY);
-            } else if (mShortcutAction != null && intent.hasExtra(Insert.PHONE)) {
-                mMode = MODE_QUERY_PICK_PHONE;
-                mQueryMode = QUERY_MODE_TEL;
-                mInitialFilter = intent.getStringExtra(Insert.PHONE);
-            } else {
-                mMode = MODE_QUERY_PICK;
-                mQueryMode = QUERY_MODE_NONE;
-                mShowSearchSnippets = true;
-                mInitialFilter = getIntent().getStringExtra(SearchManager.QUERY);
-            }
-            mSearchResultsMode = true;
-        // Since this is the filter activity it receives all intents
-        // dispatched from the SearchManager for security reasons
-        // so we need to re-dispatch from here to the intended target.
-        } else if (Intents.SEARCH_SUGGESTION_CLICKED.equals(action)) {
-            Uri data = intent.getData();
-            Uri telUri = null;
-            if (sContactsIdMatcher.match(data) == CONTACTS_ID) {
-                long contactId = Long.valueOf(data.getLastPathSegment());
-                final Cursor cursor = queryPhoneNumbers(contactId);
-                if (cursor != null) {
-                    if (cursor.getCount() == 1 && cursor.moveToFirst()) {
-                        int phoneNumberIndex = cursor.getColumnIndex(Phone.NUMBER);
-                        String phoneNumber = cursor.getString(phoneNumberIndex);
-                        telUri = Uri.parse("tel:" + phoneNumber);
-                    }
-                    cursor.close();
-                }
-            }
-            // See if the suggestion was clicked with a search action key (call button)
-            Intent newIntent;
-            if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG)) && telUri != null) {
-                newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, telUri);
-            } else {
-                newIntent = new Intent(Intent.ACTION_VIEW, data);
-            }
-            startActivity(newIntent);
-            finish();
-            return;
-        } else if (Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED.equals(action)) {
-            Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, intent.getData());
-            startActivity(newIntent);
-            finish();
-            return;
-        } else if (Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED.equals(action)) {
-            // TODO actually support this in EditContactActivity.
-            String number = intent.getData().getSchemeSpecificPart();
-            Intent newIntent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
-            newIntent.putExtra(Intents.Insert.PHONE, number);
-            startActivity(newIntent);
+        // Extract relevant information from the intent
+        mRequest = mIntentResolver.resolveIntent(getIntent());
+        if (!mRequest.isValid()) {
+            setResult(RESULT_CANCELED);
             finish();
             return;
         }
 
-        if (JOIN_AGGREGATE.equals(action)) {
-            if (mSearchMode) {
-                mMode = MODE_PICK_CONTACT;
-            } else {
-                mMode = MODE_JOIN_CONTACT;
-                mQueryAggregateId = intent.getLongExtra(EXTRA_AGGREGATE_ID, -1);
-                if (mQueryAggregateId == -1) {
-                    Log.e(TAG, "Intent " + action + " is missing required extra: "
-                            + EXTRA_AGGREGATE_ID);
-                    setResult(RESULT_CANCELED);
-                    finish();
-                }
-            }
+        Intent redirect = mRequest.getRedirectIntent();
+        if (redirect != null) {
+            // Need to start a different activity
+            startActivity(redirect);
+            finish();
+            return;
         }
 
-        if (mMode == MODE_UNKNOWN) {
-            mMode = MODE_DEFAULT;
-        }
+        setTitle(mRequest.getActivityTitle());
 
-        if (((mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0 || mSearchMode)
-                && !mSearchResultsMode) {
-            mShowNumberOfContacts = true;
-        }
+        onCreateFragment();
 
-        if (mMode == MODE_JOIN_CONTACT) {
-            setContentView(R.layout.contacts_list_content_join);
-            TextView blurbView = (TextView)findViewById(R.id.join_contact_blurb);
-
-            String blurb = getString(R.string.blurbJoinContactDataWith,
-                    getContactDisplayName(mQueryAggregateId));
-            blurbView.setText(blurb);
-            mJoinModeShowAllContacts = true;
-        } else if (mSearchMode) {
+        int listFragmentContainerId;
+        if (mRequest.isSearchMode()) {
             setContentView(R.layout.contacts_search_content);
-        } else if (mSearchResultsMode) {
-            setContentView(R.layout.contacts_list_search_results);
-            TextView titleText = (TextView)findViewById(R.id.search_results_for);
-            titleText.setText(Html.fromHtml(getString(R.string.search_results_for,
-                    "<b>" + mInitialFilter + "</b>")));
+            listFragmentContainerId = R.id.list_container;
+            setupSearchUI();
         } else {
-            setContentView(R.layout.contacts_list_content);
+            listFragmentContainerId = android.R.id.content;
         }
-
-        setupListView();
-        if (mSearchMode) {
-            setupSearchView();
-        }
-
-        mQueryHandler = new QueryHandler(this);
-        mJustCreated = true;
-
-        mSyncEnabled = true;
+        FragmentTransaction transaction = openFragmentTransaction();
+        transaction.add(mListFragment, listFragmentContainerId);
+        transaction.commit();
     }
 
-    /**
-     * Register an observer for provider status changes - we will need to
-     * reflect them in the UI.
-     */
-    private void registerProviderStatusObserver() {
-        getContentResolver().registerContentObserver(ProviderStatus.CONTENT_URI,
-                false, mProviderStatusObserver);
-    }
-
-    /**
-     * Register an observer for provider status changes - we will need to
-     * reflect them in the UI.
-     */
-    private void unregisterProviderStatusObserver() {
-        getContentResolver().unregisterContentObserver(mProviderStatusObserver);
-    }
-
-    private void setupListView() {
-        final ListView list = getListView();
-        final LayoutInflater inflater = getLayoutInflater();
-
-        mHighlightingAnimation =
-                new NameHighlightingAnimation(list, TEXT_HIGHLIGHTING_ANIMATION_DURATION);
-
-        // Tell list view to not show dividers. We'll do it ourself so that we can *not* show
-        // them when an A-Z headers is visible.
-        list.setDividerHeight(0);
-        list.setOnCreateContextMenuListener(this);
-
-        mAdapter = new ContactItemListAdapter(this);
-        setListAdapter(mAdapter);
-
-        if (list instanceof PinnedHeaderListView && mAdapter.getDisplaySectionHeadersEnabled()) {
-            mPinnedHeaderBackgroundColor =
-                    getResources().getColor(R.color.pinned_header_background);
-            PinnedHeaderListView pinnedHeaderList = (PinnedHeaderListView)list;
-            View pinnedHeader = inflater.inflate(R.layout.list_section, list, false);
-            pinnedHeaderList.setPinnedHeaderView(pinnedHeader);
-        }
-
-        list.setOnScrollListener(mAdapter);
-        list.setOnKeyListener(this);
-        list.setOnFocusChangeListener(this);
-        list.setOnTouchListener(this);
-
-        // We manually save/restore the listview state
-        list.setSaveEnabled(false);
-    }
-
-    /**
-     * Configures search UI.
-     */
-    private void setupSearchView() {
+    private void setupSearchUI() {
         mSearchEditText = (SearchEditText)findViewById(R.id.search_src_text);
-        mSearchEditText.addTextChangedListener(this);
-        mSearchEditText.setOnEditorActionListener(this);
-        mSearchEditText.setText(mInitialFilter);
-    }
-
-    private String getContactDisplayName(long contactId) {
-        String contactName = null;
-        Cursor c = getContentResolver().query(
-                ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId),
-                new String[] {Contacts.DISPLAY_NAME}, null, null, null);
-        try {
-            if (c != null && c.moveToFirst()) {
-                contactName = c.getString(0);
+        mSearchEditText.setText(mRequest.getQueryString());
+        mSearchEditText.setOnFilterTextListener(new OnFilterTextListener() {
+            public void onFilterChange(String queryString) {
+                mListFragment.setQueryString(queryString);
             }
-        } finally {
-            if (c != null) {
-                c.close();
+
+            public void onCancelSearch() {
+                finish();
             }
-        }
-
-        if (contactName == null) {
-            contactName = "";
-        }
-
-        return contactName;
-    }
-
-    private int getSummaryDisplayNameColumnIndex() {
-        if (mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
-            return SUMMARY_DISPLAY_NAME_PRIMARY_COLUMN_INDEX;
-        } else {
-            return SUMMARY_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX;
-        }
-    }
-
-    /** {@inheritDoc} */
-    public void onClick(View v) {
-        int id = v.getId();
-        switch (id) {
-            // 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) {
-                    c.moveToPosition(position);
-                    callContact(c);
-                }
-                break;
-            }
-        }
-    }
-
-    private void setEmptyText() {
-        if (mMode == MODE_JOIN_CONTACT || mSearchMode) {
-            return;
-        }
-
-        TextView empty = (TextView) findViewById(R.id.emptyText);
-        if (mDisplayOnlyPhones) {
-            empty.setText(getText(R.string.noContactsWithPhoneNumbers));
-        } else if (mMode == MODE_STREQUENT || mMode == MODE_STARRED) {
-            empty.setText(getText(R.string.noFavoritesHelpText));
-        } else if (mMode == MODE_QUERY || mMode == MODE_QUERY_PICK
-                || mMode == MODE_QUERY_PICK_PHONE || mMode == MODE_QUERY_PICK_TO_VIEW
-                || mMode == MODE_QUERY_PICK_TO_EDIT) {
-            empty.setText(getText(R.string.noMatchingContacts));
-        } else {
-            boolean hasSim = ((TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE))
-                    .hasIccCard();
-            boolean createShortcut = Intent.ACTION_CREATE_SHORTCUT.equals(getIntent().getAction());
-            if (isSyncActive()) {
-                if (createShortcut) {
-                    // Help text is the same no matter whether there is SIM or not.
-                    empty.setText(getText(R.string.noContactsHelpTextWithSyncForCreateShortcut));
-                } else if (hasSim) {
-                    empty.setText(getText(R.string.noContactsHelpTextWithSync));
-                } else {
-                    empty.setText(getText(R.string.noContactsNoSimHelpTextWithSync));
-                }
-            } else {
-                if (createShortcut) {
-                    // Help text is the same no matter whether there is SIM or not.
-                    empty.setText(getText(R.string.noContactsHelpTextForCreateShortcut));
-                } else if (hasSim) {
-                    empty.setText(getText(R.string.noContactsHelpText));
-                } else {
-                    empty.setText(getText(R.string.noContactsNoSimHelpText));
-                }
-            }
-        }
-    }
-
-    private boolean isSyncActive() {
-        Account[] accounts = AccountManager.get(this).getAccounts();
-        if (accounts != null && accounts.length > 0) {
-            IContentService contentService = ContentResolver.getContentService();
-            for (Account account : accounts) {
-                try {
-                    if (contentService.isSyncActive(account, ContactsContract.AUTHORITY)) {
-                        return true;
-                    }
-                } catch (RemoteException e) {
-                    Log.e(TAG, "Could not get the sync status");
-                }
-            }
-        }
-        return false;
-    }
-
-    private void buildUserGroupUri(String group) {
-        mGroupUri = Uri.withAppendedPath(Contacts.CONTENT_GROUP_URI, group);
-    }
-
-    /**
-     * Sets the mode when the request is for "default"
-     */
-    private void setDefaultMode() {
-        // Load the preferences
-        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
-
-        mDisplayOnlyPhones = prefs.getBoolean(Prefs.DISPLAY_ONLY_PHONES,
-                Prefs.DISPLAY_ONLY_PHONES_DEFAULT);
-    }
-
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-        mPhotoLoader.stop();
-    }
-
-    @Override
-    protected void onPause() {
-        super.onPause();
-        unregisterProviderStatusObserver();
+        });
     }
 
     @Override
     protected void onResume() {
         super.onResume();
-
-        registerProviderStatusObserver();
-        mPhotoLoader.resume();
-
-        Activity parent = getParent();
-
-        // Do this before setting the filter. The filter thread relies
-        // on some state that is initialized in setDefaultMode
-        if (mMode == MODE_DEFAULT) {
-            // If we're in default mode we need to possibly reset the mode due to a change
-            // in the preferences activity while we weren't running
-            setDefaultMode();
-        }
-
-        // See if we were invoked with a filter
-        if (mSearchMode) {
+        if (mRequest.isSearchMode()) {
             mSearchEditText.requestFocus();
         }
-
-        if (!mSearchMode && !checkProviderState(mJustCreated)) {
-            return;
-        }
-
-        if (mJustCreated) {
-            // We need to start a query here the first time the activity is launched, as long
-            // as we aren't doing a filter.
-            startQuery();
-        }
-        mJustCreated = false;
-        mSearchInitiated = false;
     }
 
     /**
-     * Obtains the contacts provider status and configures the UI accordingly.
-     *
-     * @param loadData true if the method needs to start a query when the
-     *            provider is in the normal state
-     * @return true if the provider status is normal
+     * Creates the fragment based on the current request.
      */
-    private boolean checkProviderState(boolean loadData) {
-        View importFailureView = findViewById(R.id.import_failure);
-        if (importFailureView == null) {
-            return true;
-        }
+    private void onCreateFragment() {
+        mActionCode = mRequest.getActionCode();
+        switch (mActionCode) {
+            case ContactsRequest.ACTION_DEFAULT:
+            case ContactsRequest.ACTION_INSERT_OR_EDIT_CONTACT: {
+                DefaultContactBrowseListFragment fragment = new DefaultContactBrowseListFragment();
+                fragment.setOnContactListActionListener(new ContactBrowserActionListener());
 
-        TextView messageView = (TextView) findViewById(R.id.emptyText);
-
-        // This query can be performed on the UI thread because
-        // the API explicitly allows such use.
-        Cursor cursor = getContentResolver().query(ProviderStatus.CONTENT_URI, new String[] {
-                ProviderStatus.STATUS, ProviderStatus.DATA1
-        }, null, null, null);
-        try {
-            if (cursor.moveToFirst()) {
-                int status = cursor.getInt(0);
-                if (status != mProviderStatus) {
-                    mProviderStatus = status;
-                    switch (status) {
-                        case ProviderStatus.STATUS_NORMAL:
-                            mAdapter.notifyDataSetInvalidated();
-                            if (loadData) {
-                                startQuery();
-                            }
-                            break;
-
-                        case ProviderStatus.STATUS_CHANGING_LOCALE:
-                            messageView.setText(R.string.locale_change_in_progress);
-                            mAdapter.changeCursor(null);
-                            mAdapter.notifyDataSetInvalidated();
-                            break;
-
-                        case ProviderStatus.STATUS_UPGRADING:
-                            messageView.setText(R.string.upgrade_in_progress);
-                            mAdapter.changeCursor(null);
-                            mAdapter.notifyDataSetInvalidated();
-                            break;
-
-                        case ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY:
-                            long size = cursor.getLong(1);
-                            String message = getResources().getString(
-                                    R.string.upgrade_out_of_memory, new Object[] {size});
-                            messageView.setText(message);
-                            configureImportFailureView(importFailureView);
-                            mAdapter.changeCursor(null);
-                            mAdapter.notifyDataSetInvalidated();
-                            break;
-                    }
+                if (mActionCode == ContactsRequest.ACTION_INSERT_OR_EDIT_CONTACT) {
+                    fragment.setEditMode(true);
+                    fragment.setCreateContactEnabled(true);
                 }
+
+                fragment.setDisplayWithPhonesOnlyOption(mRequest.getDisplayWithPhonesOnlyOption());
+
+                fragment.setVisibleContactsRestrictionEnabled(
+                        !mRequest.isSearchResultsMode()
+                        && mRequest.getDisplayOnlyVisible());
+
+                fragment.setContextMenuAdapter(new ContactBrowseListContextMenuAdapter(fragment));
+                fragment.setSearchMode(mRequest.isSearchMode());
+                fragment.setSearchResultsMode(mRequest.isSearchResultsMode());
+                fragment.setQueryString(mRequest.getQueryString());
+                mListFragment = fragment;
+                break;
             }
-        } finally {
-            cursor.close();
-        }
 
-        importFailureView.setVisibility(
-                mProviderStatus == ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY
-                        ? View.VISIBLE
-                        : View.GONE);
-        return mProviderStatus == ProviderStatus.STATUS_NORMAL;
-    }
+            case ContactsRequest.ACTION_GROUP: {
+                throw new UnsupportedOperationException("Not yet implemented");
+            }
 
-    private void configureImportFailureView(View importFailureView) {
+            case ContactsRequest.ACTION_STARRED: {
+                StrequentContactListFragment fragment = new StrequentContactListFragment();
+                fragment.setOnContactListActionListener(new ContactBrowserActionListener());
+                fragment.setFrequentlyContactedContactsIncluded(false);
+                fragment.setStarredContactsIncluded(true);
+                mListFragment = fragment;
+                break;
+            }
 
-        OnClickListener listener = new OnClickListener(){
+            case ContactsRequest.ACTION_FREQUENT: {
+                StrequentContactListFragment fragment = new StrequentContactListFragment();
+                fragment.setOnContactListActionListener(new ContactBrowserActionListener());
+                fragment.setFrequentlyContactedContactsIncluded(true);
+                fragment.setStarredContactsIncluded(false);
+                mListFragment = fragment;
+                break;
+            }
 
-            public void onClick(View v) {
-                switch(v.getId()) {
-                    case R.id.import_failure_uninstall_apps: {
-                        startActivity(new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS));
-                        break;
-                    }
-                    case R.id.import_failure_retry_upgrade: {
-                        // Send a provider status update, which will trigger a retry
-                        ContentValues values = new ContentValues();
-                        values.put(ProviderStatus.STATUS, ProviderStatus.STATUS_UPGRADING);
-                        getContentResolver().update(ProviderStatus.CONTENT_URI, values, null, null);
-                        break;
-                    }
-                }
-            }};
+            case ContactsRequest.ACTION_STREQUENT: {
+                StrequentContactListFragment fragment = new StrequentContactListFragment();
+                fragment.setOnContactListActionListener(new ContactBrowserActionListener());
+                fragment.setFrequentlyContactedContactsIncluded(true);
+                fragment.setStarredContactsIncluded(true);
+                mListFragment = fragment;
+                break;
+            }
 
-        Button uninstallApps = (Button) findViewById(R.id.import_failure_uninstall_apps);
-        uninstallApps.setOnClickListener(listener);
+            case ContactsRequest.ACTION_PICK_CONTACT: {
+                ContactPickerFragment fragment = new ContactPickerFragment();
+                fragment.setOnContactPickerActionListener(new ContactPickerActionListener());
+                fragment.setLegacyCompatibilityMode(mRequest.isLegacyCompatibilityMode());
+                mListFragment = fragment;
+                break;
+            }
 
-        Button retryUpgrade = (Button) findViewById(R.id.import_failure_retry_upgrade);
-        retryUpgrade.setOnClickListener(listener);
-    }
+            case ContactsRequest.ACTION_PICK_OR_CREATE_CONTACT: {
+                ContactPickerFragment fragment = new ContactPickerFragment();
+                fragment.setOnContactPickerActionListener(new ContactPickerActionListener());
+                fragment.setCreateContactEnabled(true);
+                fragment.setLegacyCompatibilityMode(mRequest.isLegacyCompatibilityMode());
+                mListFragment = fragment;
+                break;
+            }
 
-    private String getTextFilter() {
-        if (mSearchEditText != null) {
-            return mSearchEditText.getText().toString();
-        }
-        return null;
-    }
+            case ContactsRequest.ACTION_CREATE_SHORTCUT_CONTACT: {
+                ContactPickerFragment fragment = new ContactPickerFragment();
+                fragment.setOnContactPickerActionListener(new ContactPickerActionListener());
+                fragment.setCreateContactEnabled(true);
+                fragment.setLegacyCompatibilityMode(mRequest.isLegacyCompatibilityMode());
+                fragment.setShortcutRequested(true);
+                mListFragment = fragment;
+                break;
+            }
 
-    @Override
-    protected void onRestart() {
-        super.onRestart();
+            case ContactsRequest.ACTION_PICK_PHONE: {
+                PhoneNumberPickerFragment fragment = new PhoneNumberPickerFragment();
+                fragment.setOnPhoneNumberPickerActionListener(
+                        new PhoneNumberPickerActionListener());
+                fragment.setLegacyCompatibilityMode(mRequest.isLegacyCompatibilityMode());
+                mListFragment = fragment;
+                break;
+            }
 
-        if (!checkProviderState(false)) {
-            return;
-        }
+            case ContactsRequest.ACTION_CREATE_SHORTCUT_CALL: {
+                PhoneNumberPickerFragment fragment = new PhoneNumberPickerFragment();
+                fragment.setOnPhoneNumberPickerActionListener(
+                        new PhoneNumberPickerActionListener());
+                fragment.setLegacyCompatibilityMode(mRequest.isLegacyCompatibilityMode());
+                fragment.setSectionHeaderDisplayEnabled(false);
+                fragment.setShortcutAction(Intent.ACTION_CALL);
+                mListFragment = fragment;
+                break;
+            }
 
-        // The cursor was killed off in onStop(), so we need to get a new one here
-        // We do not perform the query if a filter is set on the list because the
-        // filter will cause the query to happen anyway
-        if (TextUtils.isEmpty(getTextFilter())) {
-            startQuery();
-        } else {
-            // Run the filtered query on the adapter
-            mAdapter.onContentChanged();
+            case ContactsRequest.ACTION_CREATE_SHORTCUT_SMS: {
+                PhoneNumberPickerFragment fragment = new PhoneNumberPickerFragment();
+                fragment.setOnPhoneNumberPickerActionListener(
+                        new PhoneNumberPickerActionListener());
+                fragment.setLegacyCompatibilityMode(mRequest.isLegacyCompatibilityMode());
+                fragment.setSectionHeaderDisplayEnabled(false);
+                fragment.setShortcutAction(Intent.ACTION_SENDTO);
+                mListFragment = fragment;
+                break;
+            }
+
+            case ContactsRequest.ACTION_PICK_POSTAL: {
+                PostalAddressPickerFragment fragment = new PostalAddressPickerFragment();
+                fragment.setOnPostalAddressPickerActionListener(
+                        new PostalAddressPickerActionListener());
+                fragment.setLegacyCompatibilityMode(mRequest.isLegacyCompatibilityMode());
+                mListFragment = fragment;
+                break;
+            }
+
+            default:
+                throw new IllegalStateException("Invalid action code: " + mActionCode);
         }
     }
 
-    @Override
-    protected void onSaveInstanceState(Bundle icicle) {
-        super.onSaveInstanceState(icicle);
-        // Save list state in the bundle so we can restore it after the QueryHandler has run
-        if (mList != null) {
-            icicle.putParcelable(LIST_STATE_KEY, mList.onSaveInstanceState());
+    private final class ContactBrowserActionListener implements OnContactBrowserActionListener {
+        public void onSearchAllContactsAction(String queryString) {
+            searchAllContacts(queryString, false);
+        }
+
+        public void onViewContactAction(Uri contactLookupUri) {
+            startActivity(new Intent(Intent.ACTION_VIEW, contactLookupUri));
+        }
+
+        public void onCreateNewContactAction() {
+            Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
+            Bundle extras = getIntent().getExtras();
+            if (extras != null) {
+                intent.putExtras(extras);
+            }
+            startActivity(intent);
+        }
+
+        public void onEditContactAction(Uri contactLookupUri) {
+            Intent intent = new Intent(Intent.ACTION_EDIT, contactLookupUri);
+            Bundle extras = getIntent().getExtras();
+            if (extras != null) {
+                intent.putExtras(extras);
+            }
+            startActivity(intent);
+        }
+
+        public void onAddToFavoritesAction(Uri contactUri) {
+            ContentValues values = new ContentValues(1);
+            values.put(Contacts.STARRED, 1);
+            getContentResolver().update(contactUri, values, null, null);
+        }
+
+        public void onRemoveFromFavoritesAction(Uri contactUri) {
+            ContentValues values = new ContentValues(1);
+            values.put(Contacts.STARRED, 0);
+            getContentResolver().update(contactUri, values, null, null);
+        }
+
+        public void onCallContactAction(Uri contactUri) {
+            getCallOrSmsInitiator().initiateCall(contactUri);
+        }
+
+        public void onSmsContactAction(Uri contactUri) {
+            getCallOrSmsInitiator().initiateSms(contactUri);
+        }
+
+        public void onDeleteContactAction(Uri contactUri) {
+            doContactDelete(contactUri);
+        }
+
+        public void onFinishAction() {
+            onBackPressed();
         }
     }
 
-    @Override
-    protected void onRestoreInstanceState(Bundle icicle) {
-        super.onRestoreInstanceState(icicle);
-        // Retrieve list state. This will be applied after the QueryHandler has run
-        mListState = icicle.getParcelable(LIST_STATE_KEY);
+    private final class ContactPickerActionListener implements OnContactPickerActionListener {
+        public void onSearchAllContactsAction(String queryString) {
+            searchAllContacts(queryString, true);
+        }
+
+        public void onCreateNewContactAction() {
+            Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
+            startActivityAndForwardResult(intent);
+        }
+
+        public void onPickContactAction(Uri contactUri) {
+            Intent intent = new Intent();
+            setResult(RESULT_OK, intent.setData(contactUri));
+            finish();
+        }
+
+        public void onShortcutIntentCreated(Intent intent) {
+            setResult(RESULT_OK, intent);
+            finish();
+        }
     }
 
-    @Override
-    protected void onStop() {
-        super.onStop();
-
-        mAdapter.setSuggestionsCursor(null);
-        mAdapter.changeCursor(null);
-
-        if (mMode == MODE_QUERY) {
-            // Make sure the search box is closed
-            SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
-            searchManager.stopSearch();
+    private final class PhoneNumberPickerActionListener implements
+            OnPhoneNumberPickerActionListener {
+        public void onSearchAllContactsAction(String queryString) {
+            searchAllContacts(queryString, true);
         }
+
+        public void onPickPhoneNumberAction(Uri dataUri) {
+            Intent intent = new Intent();
+            setResult(RESULT_OK, intent.setData(dataUri));
+            finish();
+        }
+
+        public void onShortcutIntentCreated(Intent intent) {
+            setResult(RESULT_OK, intent);
+            finish();
+        }
+    }
+
+    private final class PostalAddressPickerActionListener implements
+            OnPostalAddressPickerActionListener {
+        public void onSearchAllContactsAction(String queryString) {
+            searchAllContacts(queryString, true);
+        }
+
+        public void onPickPostalAddressAction(Uri dataUri) {
+            Intent intent = new Intent();
+            setResult(RESULT_OK, intent.setData(dataUri));
+            finish();
+        }
+    }
+
+    public void startActivityAndForwardResult(final Intent intent) {
+        intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
+
+        // Forward extras to the new activity
+        Bundle extras = getIntent().getExtras();
+        if (extras != null) {
+            intent.putExtras(extras);
+        }
+        startActivity(intent);
+        finish();
     }
 
     @Override
     public boolean onCreateOptionsMenu(Menu menu) {
         super.onCreateOptionsMenu(menu);
 
-        // If Contacts was invoked by another Activity simply as a way of
-        // picking a contact, don't show the options menu
-        if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) {
-            return false;
+        // TODO what other actions need the menu?
+        if (mActionCode == ContactsRequest.ACTION_DEFAULT ||
+                mActionCode == ContactsRequest.ACTION_STREQUENT) {
+            MenuInflater inflater = getMenuInflater();
+            inflater.inflate(R.menu.list, menu);
         }
-
-        MenuInflater inflater = getMenuInflater();
-        inflater.inflate(R.menu.list, menu);
         return true;
     }
 
     @Override
     public boolean onPrepareOptionsMenu(Menu menu) {
-        final boolean defaultMode = (mMode == MODE_DEFAULT);
-        menu.findItem(R.id.menu_display_groups).setVisible(defaultMode);
+        menu.findItem(R.id.menu_display_groups).setVisible(
+                mActionCode == ContactsRequest.ACTION_DEFAULT);
         return true;
     }
 
@@ -1260,7 +491,7 @@
             }
             case R.id.menu_accounts: {
                 final Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS);
-                intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] {
+                intent.putExtra(Settings.EXTRA_AUTHORITIES, new String[] {
                     ContactsContract.AUTHORITY
                 });
                 startActivity(intent);
@@ -1273,74 +504,42 @@
     @Override
     public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData,
             boolean globalSearch) {
-        if (mProviderStatus != ProviderStatus.STATUS_NORMAL) {
-            return;
-        }
+// TODO
+//        if (mProviderStatus != ProviderStatus.STATUS_NORMAL) {
+//            return;
+//        }
 
         if (globalSearch) {
             super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
         } else {
-            if (!mSearchMode && (mMode & MODE_MASK_NO_FILTER) == 0) {
-                if ((mMode & MODE_MASK_PICKER) != 0) {
-                    ContactsSearchManager.startSearchForResult(this, initialQuery,
-                            SUBACTIVITY_FILTER);
-                } else {
-                    ContactsSearchManager.startSearch(this, initialQuery);
-                }
-            }
+//            if (!mSearchMode && (mMode & MODE_MASK_NO_FILTER) == 0) {
+//                if ((mMode & MODE_MASK_PICKER) != 0) {
+//                    ContactsSearchManager.startSearchForResult(this, initialQuery,
+//                            SUBACTIVITY_FILTER, null);
+//                } else {
+                    ContactsSearchManager.startSearch(this, initialQuery, mRequest);
+//                }
+//            }
         }
     }
 
     /**
-     * Performs filtering of the list based on the search query entered in the
-     * search text edit.
-     */
-    protected void onSearchTextChanged() {
-        // Set the proper empty string
-        setEmptyText();
-
-        Filter filter = mAdapter.getFilter();
-        filter.filter(getTextFilter());
-    }
-
-    /**
      * Starts a new activity that will run a search query and display search results.
      */
-    private void doSearch() {
-        String query = getTextFilter();
+    protected void searchAllContacts(String queryString, boolean returnResult) {
+        String query = mListFragment.getQueryString();
         if (TextUtils.isEmpty(query)) {
             return;
         }
 
         Intent intent = new Intent(this, SearchResultsActivity.class);
-        Intent originalIntent = getIntent();
-        Bundle originalExtras = originalIntent.getExtras();
-        if (originalExtras != null) {
-            intent.putExtras(originalExtras);
-        }
-
+        intent.setAction(Intent.ACTION_SEARCH);
         intent.putExtra(SearchManager.QUERY, query);
-        if ((mMode & MODE_MASK_PICKER) != 0) {
-            intent.setAction(ACTION_SEARCH_INTERNAL);
-            intent.putExtra(SHORTCUT_ACTION_KEY, mShortcutAction);
-            if (mShortcutAction != null) {
-                if (Intent.ACTION_CALL.equals(mShortcutAction)
-                        || Intent.ACTION_SENDTO.equals(mShortcutAction)) {
-                    intent.putExtra(Insert.PHONE, query);
-                }
-            } else {
-                switch (mQueryMode) {
-                    case QUERY_MODE_MAILTO:
-                        intent.putExtra(Insert.EMAIL, query);
-                        break;
-                    case QUERY_MODE_TEL:
-                        intent.putExtra(Insert.PHONE, query);
-                        break;
-                }
-            }
+        intent.putExtra(ContactsSearchManager.ORIGINAL_REQUEST_KEY, mRequest);
+
+        if (returnResult) {
             startActivityForResult(intent, SUBACTIVITY_SEARCH);
         } else {
-            intent.setAction(Intent.ACTION_SEARCH);
             startActivity(intent);
         }
     }
@@ -1478,7 +677,7 @@
 
     private void doShareVisibleContacts() {
         final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI,
-                sLookupProjection, getContactSelection(), null, null);
+                sLookupProjection, Contacts.IN_VISIBLE_GROUP + "!=0", null, null);
         try {
             if (!cursor.moveToFirst()) {
                 Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show();
@@ -1522,129 +721,47 @@
         AccountSelectionUtil.doImport(this, resId, (size == 1 ? accountList.get(0) : null));
     }
 
-    @Override
-    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-        switch (requestCode) {
-            case SUBACTIVITY_NEW_CONTACT:
-                if (resultCode == RESULT_OK) {
-                    returnPickerResult(null, data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME),
-                            data.getData());
-                }
-                break;
-
-            case SUBACTIVITY_VIEW_CONTACT:
-                if (resultCode == RESULT_OK) {
-                    mAdapter.notifyDataSetChanged();
-                }
-                break;
-
-            case SUBACTIVITY_DISPLAY_GROUP:
-                // Mark as just created so we re-run the view query
-                mJustCreated = true;
-                break;
-
-            case SUBACTIVITY_FILTER:
-            case SUBACTIVITY_SEARCH:
-                // Pass through results of filter or search UI
-                if (resultCode == RESULT_OK) {
-                    setResult(RESULT_OK, data);
-                    finish();
-                }
-                break;
-        }
-    }
-
-    @Override
-    public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
-        // If Contacts was invoked by another Activity simply as a way of
-        // picking a contact, don't show the context menu
-        if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) {
-            return;
-        }
-
-        AdapterView.AdapterContextMenuInfo info;
-        try {
-             info = (AdapterView.AdapterContextMenuInfo) menuInfo;
-        } catch (ClassCastException e) {
-            Log.e(TAG, "bad menuInfo", e);
-            return;
-        }
-
-        Cursor cursor = (Cursor) getListAdapter().getItem(info.position);
-        if (cursor == null) {
-            // For some reason the requested item isn't available, do nothing
-            return;
-        }
-        long id = info.id;
-        Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, id);
-        long rawContactId = ContactsUtils.queryForRawContactId(getContentResolver(), id);
-        Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
-
-        // Setup the menu header
-        menu.setHeaderTitle(cursor.getString(getSummaryDisplayNameColumnIndex()));
-
-        // View contact details
-        menu.add(0, MENU_ITEM_VIEW_CONTACT, 0, R.string.menu_viewContact)
-                .setIntent(new Intent(Intent.ACTION_VIEW, contactUri));
-
-        if (cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0) {
-            // Calling contact
-            menu.add(0, MENU_ITEM_CALL, 0,
-                    getString(R.string.menu_call));
-            // Send SMS item
-            menu.add(0, MENU_ITEM_SEND_SMS, 0, getString(R.string.menu_sendSMS));
-        }
-
-        // Star toggling
-        int starState = cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX);
-        if (starState == 0) {
-            menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_addStar);
-        } else {
-            menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_removeStar);
-        }
-
-        // Contact editing
-        menu.add(0, MENU_ITEM_EDIT, 0, R.string.menu_editContact)
-                .setIntent(new Intent(Intent.ACTION_EDIT, rawContactUri));
-        menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_deleteContact);
-    }
+//    @Override
+//    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+//        switch (requestCode) {
+//            case SUBACTIVITY_NEW_CONTACT:
+//                if (resultCode == RESULT_OK) {
+////                    returnPickerResult(null, data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME),
+////                            data.getData());
+//                }
+//                break;
+//
+//            case SUBACTIVITY_VIEW_CONTACT:
+//                if (resultCode == RESULT_OK) {
+//                    mAdapter.notifyDataSetChanged();
+//                }
+//                break;
+//
+//            case SUBACTIVITY_DISPLAY_GROUP:
+//                // Mark as just created so we re-run the view query
+////                mJustCreated = true;
+//                break;
+//
+//            case SUBACTIVITY_FILTER:
+//            case SUBACTIVITY_SEARCH:
+//                // Pass through results of filter or search UI
+//                if (resultCode == RESULT_OK) {
+//                    setResult(RESULT_OK, data);
+//                    finish();
+//                } else if (resultCode == RESULT_CANCELED && mMode == MODE_PICK_MULTIPLE_PHONES) {
+//                    // Finish the activity if the sub activity was canceled as back key is used
+//                    // to confirm user selection in MODE_PICK_MULTIPLE_PHONES.
+//                    finish();
+//                }
+//                break;
+//        }
+//    }
 
     @Override
     public boolean onContextItemSelected(MenuItem item) {
-        AdapterView.AdapterContextMenuInfo info;
-        try {
-             info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
-        } catch (ClassCastException e) {
-            Log.e(TAG, "bad menuInfo", e);
-            return false;
-        }
-
-        Cursor cursor = (Cursor) getListAdapter().getItem(info.position);
-
-        switch (item.getItemId()) {
-            case MENU_ITEM_TOGGLE_STAR: {
-                // Toggle the star
-                ContentValues values = new ContentValues(1);
-                values.put(Contacts.STARRED, cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX) == 0 ? 1 : 0);
-                final Uri selectedUri = this.getContactUri(info.position);
-                getContentResolver().update(selectedUri, values, null, null);
-                return true;
-            }
-
-            case MENU_ITEM_CALL: {
-                callContact(cursor);
-                return true;
-            }
-
-            case MENU_ITEM_SEND_SMS: {
-                smsContact(cursor);
-                return true;
-            }
-
-            case MENU_ITEM_DELETE: {
-                doContactDelete(getContactUri(info.position));
-                return true;
-            }
+        ContextMenuAdapter menuAdapter = mListFragment.getContextMenuAdapter();
+        if (menuAdapter != null) {
+            return menuAdapter.onContextItemSelected(item);
         }
 
         return super.onContextItemSelected(item);
@@ -1654,8 +771,10 @@
      * Event handler for the use case where the user starts typing without
      * bringing up the search UI first.
      */
-    public boolean onKey(View v, int keyCode, KeyEvent event) {
-        if (!mSearchMode && (mMode & MODE_MASK_NO_FILTER) == 0 && !mSearchInitiated) {
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        if (!mSearchInitiated && !mRequest.isSearchMode()
+                && !mRequest.isSearchResultsMode()) {
             int unicodeChar = event.getUnicodeChar();
             if (unicodeChar != 0) {
                 mSearchInitiated = true;
@@ -1663,45 +782,19 @@
                 return true;
             }
         }
-        return false;
-    }
-
-    /**
-     * Event handler for search UI.
-     */
-    public void afterTextChanged(Editable s) {
-        onSearchTextChanged();
-    }
-
-    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-    }
-
-    public void onTextChanged(CharSequence s, int start, int before, int count) {
-    }
-
-    /**
-     * Event handler for search UI.
-     */
-    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
-        if (actionId == EditorInfo.IME_ACTION_DONE) {
-            hideSoftKeyboard();
-            if (TextUtils.isEmpty(getTextFilter())) {
-                finish();
-            }
-            return true;
-        }
-        return false;
+        return super.dispatchKeyEvent(event);
     }
 
     @Override
     public boolean onKeyDown(int keyCode, KeyEvent event) {
+        // TODO move to the fragment
         switch (keyCode) {
-            case KeyEvent.KEYCODE_CALL: {
-                if (callSelection()) {
-                    return true;
-                }
-                break;
-            }
+//            case KeyEvent.KEYCODE_CALL: {
+//                if (callSelection()) {
+//                    return true;
+//                }
+//                break;
+//            }
 
             case KeyEvent.KEYCODE_DEL: {
                 if (deleteSelection()) {
@@ -1715,18 +808,17 @@
     }
 
     private boolean deleteSelection() {
-        if ((mMode & MODE_MASK_PICKER) != 0) {
-            return false;
-        }
-
-        final int position = getListView().getSelectedItemPosition();
-        if (position != ListView.INVALID_POSITION) {
-            Uri contactUri = getContactUri(position);
-            if (contactUri != null) {
-                doContactDelete(contactUri);
-                return true;
-            }
-        }
+        // TODO move to the fragment
+//        if (mActionCode == ContactsRequest.ACTION_DEFAULT) {
+//            final int position = mListView.getSelectedItemPosition();
+//            if (position != ListView.INVALID_POSITION) {
+//                Uri contactUri = getContactUri(position);
+//                if (contactUri != null) {
+//                    doContactDelete(contactUri);
+//                    return true;
+//                }
+//            }
+//        }
         return false;
     }
 
@@ -1773,1813 +865,10 @@
         }
     }
 
-    /**
-     * Dismisses the soft keyboard when the list takes focus.
-     */
-    public void onFocusChange(View view, boolean hasFocus) {
-        if (view == getListView() && hasFocus) {
-            hideSoftKeyboard();
+    private CallOrSmsInitiator getCallOrSmsInitiator() {
+        if (mCallOrSmsInitiator == null) {
+            mCallOrSmsInitiator = new CallOrSmsInitiator(this);
         }
-    }
-
-    /**
-     * Dismisses the soft keyboard when the list takes focus.
-     */
-    public boolean onTouch(View view, MotionEvent event) {
-        if (view == getListView()) {
-            hideSoftKeyboard();
-        }
-        return false;
-    }
-
-    /**
-     * Dismisses the search UI along with the keyboard if the filter text is empty.
-     */
-    public boolean onKeyPreIme(int keyCode, KeyEvent event) {
-        if (mSearchMode && keyCode == KeyEvent.KEYCODE_BACK && TextUtils.isEmpty(getTextFilter())) {
-            hideSoftKeyboard();
-            onBackPressed();
-            return true;
-        }
-        return false;
-    }
-
-    @Override
-    protected void onListItemClick(ListView l, View v, int position, long id) {
-        hideSoftKeyboard();
-
-        if (mSearchMode && mAdapter.isSearchAllContactsItemPosition(position)) {
-            doSearch();
-        } else if (mMode == MODE_INSERT_OR_EDIT_CONTACT || mMode == MODE_QUERY_PICK_TO_EDIT) {
-            Intent intent;
-            if (position == 0 && !mSearchMode && mMode != MODE_QUERY_PICK_TO_EDIT) {
-                intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
-            } else {
-                intent = new Intent(Intent.ACTION_EDIT, getSelectedUri(position));
-            }
-            intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
-            Bundle extras = getIntent().getExtras();
-            if (extras != null) {
-                intent.putExtras(extras);
-            }
-            intent.putExtra(KEY_PICKER_MODE, (mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER);
-
-            startActivity(intent);
-            finish();
-        } else if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW
-                && position == 0) {
-            Intent newContact = new Intent(Intents.Insert.ACTION, Contacts.CONTENT_URI);
-            startActivityForResult(newContact, SUBACTIVITY_NEW_CONTACT);
-        } else if (mMode == MODE_JOIN_CONTACT && id == JOIN_MODE_SHOW_ALL_CONTACTS_ID) {
-            mJoinModeShowAllContacts = false;
-            startQuery();
-        } else if (id > 0) {
-            final Uri uri = getSelectedUri(position);
-            if ((mMode & MODE_MASK_PICKER) == 0) {
-                final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
-                startActivityForResult(intent, SUBACTIVITY_VIEW_CONTACT);
-            } else if (mMode == MODE_JOIN_CONTACT) {
-                returnPickerResult(null, null, uri);
-            } else if (mMode == MODE_QUERY_PICK_TO_VIEW) {
-                // Started with query that should launch to view contact
-                final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
-                startActivity(intent);
-                finish();
-            } else if (mMode == MODE_PICK_PHONE || mMode == MODE_QUERY_PICK_PHONE) {
-                Cursor c = (Cursor) mAdapter.getItem(position);
-                returnPickerResult(c, c.getString(PHONE_DISPLAY_NAME_COLUMN_INDEX), uri);
-            } else if ((mMode & MODE_MASK_PICKER) != 0) {
-                Cursor c = (Cursor) mAdapter.getItem(position);
-                returnPickerResult(c, c.getString(getSummaryDisplayNameColumnIndex()), uri);
-            } else if (mMode == MODE_PICK_POSTAL
-                    || mMode == MODE_LEGACY_PICK_POSTAL
-                    || mMode == MODE_LEGACY_PICK_PHONE) {
-                returnPickerResult(null, null, uri);
-            }
-        } else {
-            signalError();
-        }
-    }
-
-    private void hideSoftKeyboard() {
-        // Hide soft keyboard, if visible
-        InputMethodManager inputMethodManager = (InputMethodManager)
-                getSystemService(Context.INPUT_METHOD_SERVICE);
-        inputMethodManager.hideSoftInputFromWindow(mList.getWindowToken(), 0);
-    }
-
-    /**
-     * @param selectedUri In most cases, this should be a lookup {@link Uri}, possibly
-     *            generated through {@link Contacts#getLookupUri(long, String)}.
-     */
-    private void returnPickerResult(Cursor c, String name, Uri selectedUri) {
-        final Intent intent = new Intent();
-
-        if (mShortcutAction != null) {
-            Intent shortcutIntent;
-            if (Intent.ACTION_VIEW.equals(mShortcutAction)) {
-                // This is a simple shortcut to view a contact.
-                shortcutIntent = new Intent(ContactsContract.QuickContact.ACTION_QUICK_CONTACT);
-                shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
-                        Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
-
-                shortcutIntent.setData(selectedUri);
-                shortcutIntent.putExtra(ContactsContract.QuickContact.EXTRA_MODE,
-                        ContactsContract.QuickContact.MODE_LARGE);
-                shortcutIntent.putExtra(ContactsContract.QuickContact.EXTRA_EXCLUDE_MIMES,
-                        (String[]) null);
-
-                final Bitmap icon = framePhoto(loadContactPhoto(selectedUri, null));
-                if (icon != null) {
-                    intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, scaleToAppIconSize(icon));
-                } else {
-                    intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
-                            Intent.ShortcutIconResource.fromContext(this,
-                                    R.drawable.ic_launcher_shortcut_contact));
-                }
-            } else {
-                // This is a direct dial or sms shortcut.
-                String number = c.getString(PHONE_NUMBER_COLUMN_INDEX);
-                int type = c.getInt(PHONE_TYPE_COLUMN_INDEX);
-                String scheme;
-                int resid;
-                if (Intent.ACTION_CALL.equals(mShortcutAction)) {
-                    scheme = Constants.SCHEME_TEL;
-                    resid = R.drawable.badge_action_call;
-                } else {
-                    scheme = Constants.SCHEME_SMSTO;
-                    resid = R.drawable.badge_action_sms;
-                }
-
-                // Make the URI a direct tel: URI so that it will always continue to work
-                Uri phoneUri = Uri.fromParts(scheme, number, null);
-                shortcutIntent = new Intent(mShortcutAction, phoneUri);
-
-                intent.putExtra(Intent.EXTRA_SHORTCUT_ICON,
-                        generatePhoneNumberIcon(selectedUri, type, resid));
-            }
-            shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-            intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
-            intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
-            setResult(RESULT_OK, intent);
-        } else {
-            intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
-            setResult(RESULT_OK, intent.setData(selectedUri));
-        }
-        finish();
-    }
-
-    private Bitmap framePhoto(Bitmap photo) {
-        final Resources r = getResources();
-        final Drawable frame = r.getDrawable(com.android.internal.R.drawable.quickcontact_badge);
-
-        final int width = r.getDimensionPixelSize(R.dimen.contact_shortcut_frame_width);
-        final int height = r.getDimensionPixelSize(R.dimen.contact_shortcut_frame_height);
-
-        frame.setBounds(0, 0, width, height);
-
-        final Rect padding = new Rect();
-        frame.getPadding(padding);
-
-        final Rect source = new Rect(0, 0, photo.getWidth(), photo.getHeight());
-        final Rect destination = new Rect(padding.left, padding.top,
-                width - padding.right, height - padding.bottom);
-
-        final int d = Math.max(width, height);
-        final Bitmap b = Bitmap.createBitmap(d, d, Bitmap.Config.ARGB_8888);
-        final Canvas c = new Canvas(b);
-
-        c.translate((d - width) / 2.0f, (d - height) / 2.0f);
-        frame.draw(c);
-        c.drawBitmap(photo, source, destination, new Paint(Paint.FILTER_BITMAP_FLAG));
-
-        return b;
-    }
-
-    /**
-     * Generates a phone number shortcut icon. Adds an overlay describing the type of the phone
-     * number, and if there is a photo also adds the call action icon.
-     *
-     * @param lookupUri The person the phone number belongs to
-     * @param type The type of the phone number
-     * @param actionResId The ID for the action resource
-     * @return The bitmap for the icon
-     */
-    private Bitmap generatePhoneNumberIcon(Uri lookupUri, int type, int actionResId) {
-        final Resources r = getResources();
-        boolean drawPhoneOverlay = true;
-        final float scaleDensity = getResources().getDisplayMetrics().scaledDensity;
-
-        Bitmap photo = loadContactPhoto(lookupUri, null);
-        if (photo == null) {
-            // If there isn't a photo use the generic phone action icon instead
-            Bitmap phoneIcon = getPhoneActionIcon(r, actionResId);
-            if (phoneIcon != null) {
-                photo = phoneIcon;
-                drawPhoneOverlay = false;
-            } else {
-                return null;
-            }
-        }
-
-        // Setup the drawing classes
-        Bitmap icon = createShortcutBitmap();
-        Canvas canvas = new Canvas(icon);
-
-        // Copy in the photo
-        Paint photoPaint = new Paint();
-        photoPaint.setDither(true);
-        photoPaint.setFilterBitmap(true);
-        Rect src = new Rect(0,0, photo.getWidth(),photo.getHeight());
-        Rect dst = new Rect(0,0, mIconSize, mIconSize);
-        canvas.drawBitmap(photo, src, dst, photoPaint);
-
-        // Create an overlay for the phone number type
-        String overlay = null;
-        switch (type) {
-            case Phone.TYPE_HOME:
-                overlay = getString(R.string.type_short_home);
-                break;
-
-            case Phone.TYPE_MOBILE:
-                overlay = getString(R.string.type_short_mobile);
-                break;
-
-            case Phone.TYPE_WORK:
-                overlay = getString(R.string.type_short_work);
-                break;
-
-            case Phone.TYPE_PAGER:
-                overlay = getString(R.string.type_short_pager);
-                break;
-
-            case Phone.TYPE_OTHER:
-                overlay = getString(R.string.type_short_other);
-                break;
-        }
-        if (overlay != null) {
-            Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG);
-            textPaint.setTextSize(20.0f * scaleDensity);
-            textPaint.setTypeface(Typeface.DEFAULT_BOLD);
-            textPaint.setColor(r.getColor(R.color.textColorIconOverlay));
-            textPaint.setShadowLayer(3f, 1, 1, r.getColor(R.color.textColorIconOverlayShadow));
-            canvas.drawText(overlay, 2 * scaleDensity, 16 * scaleDensity, textPaint);
-        }
-
-        // Draw the phone action icon as an overlay
-        if (ENABLE_ACTION_ICON_OVERLAYS && drawPhoneOverlay) {
-            Bitmap phoneIcon = getPhoneActionIcon(r, actionResId);
-            if (phoneIcon != null) {
-                src.set(0, 0, phoneIcon.getWidth(), phoneIcon.getHeight());
-                int iconWidth = icon.getWidth();
-                dst.set(iconWidth - ((int) (20 * scaleDensity)), -1,
-                        iconWidth, ((int) (19 * scaleDensity)));
-                canvas.drawBitmap(phoneIcon, src, dst, photoPaint);
-            }
-        }
-
-        return icon;
-    }
-
-    private Bitmap scaleToAppIconSize(Bitmap photo) {
-        // Setup the drawing classes
-        Bitmap icon = createShortcutBitmap();
-        Canvas canvas = new Canvas(icon);
-
-        // Copy in the photo
-        Paint photoPaint = new Paint();
-        photoPaint.setDither(true);
-        photoPaint.setFilterBitmap(true);
-        Rect src = new Rect(0,0, photo.getWidth(),photo.getHeight());
-        Rect dst = new Rect(0,0, mIconSize, mIconSize);
-        canvas.drawBitmap(photo, src, dst, photoPaint);
-
-        return icon;
-    }
-
-    private Bitmap createShortcutBitmap() {
-        return Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888);
-    }
-
-    /**
-     * Returns the icon for the phone call action.
-     *
-     * @param r The resources to load the icon from
-     * @param resId The resource ID to load
-     * @return the icon for the phone call action
-     */
-    private Bitmap getPhoneActionIcon(Resources r, int resId) {
-        Drawable phoneIcon = r.getDrawable(resId);
-        if (phoneIcon instanceof BitmapDrawable) {
-            BitmapDrawable bd = (BitmapDrawable) phoneIcon;
-            return bd.getBitmap();
-        } else {
-            return null;
-        }
-    }
-
-    private Uri getUriToQuery() {
-        switch(mMode) {
-            case MODE_JOIN_CONTACT:
-                return getJoinSuggestionsUri(null);
-            case MODE_FREQUENT:
-            case MODE_STARRED:
-                return Contacts.CONTENT_URI;
-
-            case MODE_DEFAULT:
-            case MODE_CUSTOM:
-            case MODE_INSERT_OR_EDIT_CONTACT:
-            case MODE_PICK_CONTACT:
-            case MODE_PICK_OR_CREATE_CONTACT:{
-                return CONTACTS_CONTENT_URI_WITH_LETTER_COUNTS;
-            }
-            case MODE_STREQUENT: {
-                return Contacts.CONTENT_STREQUENT_URI;
-            }
-            case MODE_LEGACY_PICK_PERSON:
-            case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
-                return People.CONTENT_URI;
-            }
-            case MODE_PICK_PHONE: {
-                return buildSectionIndexerUri(Phone.CONTENT_URI);
-            }
-            case MODE_LEGACY_PICK_PHONE: {
-                return Phones.CONTENT_URI;
-            }
-            case MODE_PICK_POSTAL: {
-                return buildSectionIndexerUri(StructuredPostal.CONTENT_URI);
-            }
-            case MODE_LEGACY_PICK_POSTAL: {
-                return ContactMethods.CONTENT_URI;
-            }
-            case MODE_QUERY_PICK_TO_VIEW: {
-                if (mQueryMode == QUERY_MODE_MAILTO) {
-                    return Uri.withAppendedPath(Email.CONTENT_FILTER_URI,
-                            Uri.encode(mInitialFilter));
-                } else if (mQueryMode == QUERY_MODE_TEL) {
-                    return Uri.withAppendedPath(Phone.CONTENT_FILTER_URI,
-                            Uri.encode(mInitialFilter));
-                }
-                return CONTACTS_CONTENT_URI_WITH_LETTER_COUNTS;
-            }
-            case MODE_QUERY:
-            case MODE_QUERY_PICK:
-            case MODE_QUERY_PICK_TO_EDIT: {
-                return getContactFilterUri(mInitialFilter);
-            }
-            case MODE_QUERY_PICK_PHONE: {
-                return Uri.withAppendedPath(Phone.CONTENT_FILTER_URI,
-                        Uri.encode(mInitialFilter));
-            }
-            case MODE_GROUP: {
-                return mGroupUri;
-            }
-            default: {
-                throw new IllegalStateException("Can't generate URI: Unsupported Mode.");
-            }
-        }
-    }
-
-    /**
-     * Build the {@link Contacts#CONTENT_LOOKUP_URI} for the given
-     * {@link ListView} position, using {@link #mAdapter}.
-     */
-    private Uri getContactUri(int position) {
-        if (position == ListView.INVALID_POSITION) {
-            throw new IllegalArgumentException("Position not in list bounds");
-        }
-
-        final Cursor cursor = (Cursor)mAdapter.getItem(position);
-        if (cursor == null) {
-            return null;
-        }
-
-        switch(mMode) {
-            case MODE_LEGACY_PICK_PERSON:
-            case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
-                final long personId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
-                return ContentUris.withAppendedId(People.CONTENT_URI, personId);
-            }
-
-            default: {
-                // Build and return soft, lookup reference
-                final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
-                final String lookupKey = cursor.getString(SUMMARY_LOOKUP_KEY_COLUMN_INDEX);
-                return Contacts.getLookupUri(contactId, lookupKey);
-            }
-        }
-    }
-
-    /**
-     * Build the {@link Uri} for the given {@link ListView} position, which can
-     * be used as result when in {@link #MODE_MASK_PICKER} mode.
-     */
-    private Uri getSelectedUri(int position) {
-        if (position == ListView.INVALID_POSITION) {
-            throw new IllegalArgumentException("Position not in list bounds");
-        }
-
-        final long id = mAdapter.getItemId(position);
-        switch(mMode) {
-            case MODE_LEGACY_PICK_PERSON:
-            case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
-                return ContentUris.withAppendedId(People.CONTENT_URI, id);
-            }
-            case MODE_PICK_PHONE:
-            case MODE_QUERY_PICK_PHONE: {
-                return ContentUris.withAppendedId(Data.CONTENT_URI, id);
-            }
-            case MODE_LEGACY_PICK_PHONE: {
-                return ContentUris.withAppendedId(Phones.CONTENT_URI, id);
-            }
-            case MODE_PICK_POSTAL: {
-                return ContentUris.withAppendedId(Data.CONTENT_URI, id);
-            }
-            case MODE_LEGACY_PICK_POSTAL: {
-                return ContentUris.withAppendedId(ContactMethods.CONTENT_URI, id);
-            }
-            default: {
-                return getContactUri(position);
-            }
-        }
-    }
-
-    String[] getProjectionForQuery() {
-        switch(mMode) {
-            case MODE_JOIN_CONTACT:
-            case MODE_STREQUENT:
-            case MODE_FREQUENT:
-            case MODE_STARRED:
-            case MODE_DEFAULT:
-            case MODE_CUSTOM:
-            case MODE_INSERT_OR_EDIT_CONTACT:
-            case MODE_GROUP:
-            case MODE_PICK_CONTACT:
-            case MODE_PICK_OR_CREATE_CONTACT: {
-                return mSearchMode
-                        ? CONTACTS_SUMMARY_FILTER_PROJECTION
-                        : CONTACTS_SUMMARY_PROJECTION;
-            }
-            case MODE_QUERY:
-            case MODE_QUERY_PICK:
-            case MODE_QUERY_PICK_TO_EDIT: {
-                return CONTACTS_SUMMARY_FILTER_PROJECTION;
-            }
-            case MODE_LEGACY_PICK_PERSON:
-            case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
-                return LEGACY_PEOPLE_PROJECTION ;
-            }
-            case MODE_QUERY_PICK_PHONE:
-            case MODE_PICK_PHONE: {
-                return PHONES_PROJECTION;
-            }
-            case MODE_LEGACY_PICK_PHONE: {
-                return LEGACY_PHONES_PROJECTION;
-            }
-            case MODE_PICK_POSTAL: {
-                return POSTALS_PROJECTION;
-            }
-            case MODE_LEGACY_PICK_POSTAL: {
-                return LEGACY_POSTALS_PROJECTION;
-            }
-            case MODE_QUERY_PICK_TO_VIEW: {
-                if (mQueryMode == QUERY_MODE_MAILTO) {
-                    return CONTACTS_SUMMARY_PROJECTION_FROM_EMAIL;
-                } else if (mQueryMode == QUERY_MODE_TEL) {
-                    return PHONES_PROJECTION;
-                }
-                break;
-            }
-        }
-
-        // Default to normal aggregate projection
-        return CONTACTS_SUMMARY_PROJECTION;
-    }
-
-    private Bitmap loadContactPhoto(Uri selectedUri, BitmapFactory.Options options) {
-        Uri contactUri = null;
-        if (Contacts.CONTENT_ITEM_TYPE.equals(getContentResolver().getType(selectedUri))) {
-            // TODO we should have a "photo" directory under the lookup URI itself
-            contactUri = Contacts.lookupContact(getContentResolver(), selectedUri);
-        } else {
-
-            Cursor cursor = getContentResolver().query(selectedUri,
-                    new String[] { Data.CONTACT_ID }, null, null, null);
-            try {
-                if (cursor != null && cursor.moveToFirst()) {
-                    final long contactId = cursor.getLong(0);
-                    contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
-                }
-            } finally {
-                if (cursor != null) cursor.close();
-            }
-        }
-
-        Cursor cursor = null;
-        Bitmap bm = null;
-
-        try {
-            Uri photoUri = Uri.withAppendedPath(contactUri, Contacts.Photo.CONTENT_DIRECTORY);
-            cursor = getContentResolver().query(photoUri, new String[] {Photo.PHOTO},
-                    null, null, null);
-            if (cursor != null && cursor.moveToFirst()) {
-                bm = ContactsUtils.loadContactPhoto(cursor, 0, options);
-            }
-        } finally {
-            if (cursor != null) {
-                cursor.close();
-            }
-        }
-
-        if (bm == null) {
-            final int[] fallbacks = {
-                R.drawable.ic_contact_picture,
-                R.drawable.ic_contact_picture_2,
-                R.drawable.ic_contact_picture_3
-            };
-            bm = BitmapFactory.decodeResource(getResources(),
-                    fallbacks[new Random().nextInt(fallbacks.length)]);
-        }
-
-        return bm;
-    }
-
-    /**
-     * Return the selection arguments for a default query based on the
-     * {@link #mDisplayOnlyPhones} flag.
-     */
-    private String getContactSelection() {
-        if (mDisplayOnlyPhones) {
-            return CLAUSE_ONLY_VISIBLE + " AND " + CLAUSE_ONLY_PHONES;
-        } else {
-            return CLAUSE_ONLY_VISIBLE;
-        }
-    }
-
-    private Uri getContactFilterUri(String filter) {
-        Uri baseUri;
-        if (!TextUtils.isEmpty(filter)) {
-            baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(filter));
-        } else {
-            baseUri = Contacts.CONTENT_URI;
-        }
-
-        if (mAdapter.getDisplaySectionHeadersEnabled()) {
-            return buildSectionIndexerUri(baseUri);
-        } else {
-            return baseUri;
-        }
-    }
-
-    private Uri getPeopleFilterUri(String filter) {
-        if (!TextUtils.isEmpty(filter)) {
-            return Uri.withAppendedPath(People.CONTENT_FILTER_URI, Uri.encode(filter));
-        } else {
-            return People.CONTENT_URI;
-        }
-    }
-
-    private static Uri buildSectionIndexerUri(Uri uri) {
-        return uri.buildUpon()
-                .appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true").build();
-    }
-
-    private Uri getJoinSuggestionsUri(String filter) {
-        Builder builder = Contacts.CONTENT_URI.buildUpon();
-        builder.appendEncodedPath(String.valueOf(mQueryAggregateId));
-        builder.appendEncodedPath(AggregationSuggestions.CONTENT_DIRECTORY);
-        if (!TextUtils.isEmpty(filter)) {
-            builder.appendEncodedPath(Uri.encode(filter));
-        }
-        builder.appendQueryParameter("limit", String.valueOf(MAX_SUGGESTIONS));
-        return builder.build();
-    }
-
-    private String getSortOrder(String[] projectionType) {
-        if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
-            return Contacts.SORT_KEY_PRIMARY;
-        } else {
-            return Contacts.SORT_KEY_ALTERNATIVE;
-        }
-    }
-
-    void startQuery() {
-        // Set the proper empty string
-        setEmptyText();
-
-        if (mSearchResultsMode) {
-            TextView foundContactsText = (TextView)findViewById(R.id.search_results_found);
-            foundContactsText.setText(R.string.search_results_searching);
-        }
-
-        mAdapter.setLoading(true);
-
-        // Cancel any pending queries
-        mQueryHandler.cancelOperation(QUERY_TOKEN);
-        mQueryHandler.setLoadingJoinSuggestions(false);
-
-        mSortOrder = mContactsPrefs.getSortOrder();
-        mDisplayOrder = mContactsPrefs.getDisplayOrder();
-
-        // When sort order and display order contradict each other, we want to
-        // highlight the part of the name used for sorting.
-        mHighlightWhenScrolling = false;
-        if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_PRIMARY &&
-                mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_ALTERNATIVE) {
-            mHighlightWhenScrolling = true;
-        } else if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_ALTERNATIVE &&
-                mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
-            mHighlightWhenScrolling = true;
-        }
-
-        String[] projection = getProjectionForQuery();
-        if (mSearchMode && TextUtils.isEmpty(getTextFilter())) {
-            mAdapter.changeCursor(new MatrixCursor(projection));
-            return;
-        }
-
-        String callingPackage = getCallingPackage();
-        Uri uri = getUriToQuery();
-        if (!TextUtils.isEmpty(callingPackage)) {
-            uri = uri.buildUpon()
-                    .appendQueryParameter(ContactsContract.REQUESTING_PACKAGE_PARAM_KEY,
-                            callingPackage)
-                    .build();
-        }
-
-        // Kick off the new query
-        switch (mMode) {
-            case MODE_GROUP:
-            case MODE_DEFAULT:
-            case MODE_CUSTOM:
-            case MODE_PICK_CONTACT:
-            case MODE_PICK_OR_CREATE_CONTACT:
-            case MODE_INSERT_OR_EDIT_CONTACT:
-                mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, getContactSelection(),
-                        null, getSortOrder(projection));
-                break;
-
-            case MODE_LEGACY_PICK_PERSON:
-            case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
-                mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, null, null,
-                        People.DISPLAY_NAME);
-                break;
-            }
-            case MODE_PICK_POSTAL:
-            case MODE_QUERY:
-            case MODE_QUERY_PICK:
-            case MODE_QUERY_PICK_PHONE:
-            case MODE_QUERY_PICK_TO_VIEW:
-            case MODE_QUERY_PICK_TO_EDIT: {
-                mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, null, null,
-                        getSortOrder(projection));
-                break;
-            }
-
-            case MODE_STARRED:
-                mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
-                        projection, Contacts.STARRED + "=1", null,
-                        getSortOrder(projection));
-                break;
-
-            case MODE_FREQUENT:
-                mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
-                        projection,
-                        Contacts.TIMES_CONTACTED + " > 0", null,
-                        Contacts.TIMES_CONTACTED + " DESC, "
-                        + getSortOrder(projection));
-                break;
-
-            case MODE_STREQUENT:
-                mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, null, null, null);
-                break;
-
-            case MODE_PICK_PHONE:
-                mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
-                        projection, CLAUSE_ONLY_VISIBLE, null, getSortOrder(projection));
-                break;
-
-            case MODE_LEGACY_PICK_PHONE:
-                mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
-                        projection, null, null, Phones.DISPLAY_NAME);
-                break;
-
-            case MODE_LEGACY_PICK_POSTAL:
-                mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
-                        projection,
-                        ContactMethods.KIND + "=" + android.provider.Contacts.KIND_POSTAL, null,
-                        ContactMethods.DISPLAY_NAME);
-                break;
-
-            case MODE_JOIN_CONTACT:
-                mQueryHandler.setLoadingJoinSuggestions(true);
-                mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection,
-                        null, null, null);
-                break;
-        }
-    }
-
-    /**
-     * Called from a background thread to do the filter and return the resulting cursor.
-     *
-     * @param filter the text that was entered to filter on
-     * @return a cursor with the results of the filter
-     */
-    Cursor doFilter(String filter) {
-        String[] projection = getProjectionForQuery();
-        if (mSearchMode && TextUtils.isEmpty(getTextFilter())) {
-            return new MatrixCursor(projection);
-        }
-
-        final ContentResolver resolver = getContentResolver();
-        switch (mMode) {
-            case MODE_DEFAULT:
-            case MODE_CUSTOM:
-            case MODE_PICK_CONTACT:
-            case MODE_PICK_OR_CREATE_CONTACT:
-            case MODE_INSERT_OR_EDIT_CONTACT: {
-                return resolver.query(getContactFilterUri(filter), projection,
-                        getContactSelection(), null, getSortOrder(projection));
-            }
-
-            case MODE_LEGACY_PICK_PERSON:
-            case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
-                return resolver.query(getPeopleFilterUri(filter), projection, null, null,
-                        People.DISPLAY_NAME);
-            }
-
-            case MODE_STARRED: {
-                return resolver.query(getContactFilterUri(filter), projection,
-                        Contacts.STARRED + "=1", null,
-                        getSortOrder(projection));
-            }
-
-            case MODE_FREQUENT: {
-                return resolver.query(getContactFilterUri(filter), projection,
-                        Contacts.TIMES_CONTACTED + " > 0", null,
-                        Contacts.TIMES_CONTACTED + " DESC, "
-                        + getSortOrder(projection));
-            }
-
-            case MODE_STREQUENT: {
-                Uri uri;
-                if (!TextUtils.isEmpty(filter)) {
-                    uri = Uri.withAppendedPath(Contacts.CONTENT_STREQUENT_FILTER_URI,
-                            Uri.encode(filter));
-                } else {
-                    uri = Contacts.CONTENT_STREQUENT_URI;
-                }
-                return resolver.query(uri, projection, null, null, null);
-            }
-
-            case MODE_PICK_PHONE: {
-                Uri uri = getUriToQuery();
-                if (!TextUtils.isEmpty(filter)) {
-                    uri = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(filter));
-                }
-                return resolver.query(uri, projection, CLAUSE_ONLY_VISIBLE, null,
-                        getSortOrder(projection));
-            }
-
-            case MODE_LEGACY_PICK_PHONE: {
-                //TODO: Support filtering here (bug 2092503)
-                break;
-            }
-
-            case MODE_JOIN_CONTACT: {
-
-                // We are on a background thread. Run queries one after the other synchronously
-                Cursor cursor = resolver.query(getJoinSuggestionsUri(filter), projection, null,
-                        null, null);
-                mAdapter.setSuggestionsCursor(cursor);
-                mJoinModeShowAllContacts = false;
-                return resolver.query(getContactFilterUri(filter), projection,
-                        Contacts._ID + " != " + mQueryAggregateId + " AND " + CLAUSE_ONLY_VISIBLE,
-                        null, getSortOrder(projection));
-            }
-        }
-        throw new UnsupportedOperationException("filtering not allowed in mode " + mMode);
-    }
-
-    private Cursor getShowAllContactsLabelCursor(String[] projection) {
-        MatrixCursor matrixCursor = new MatrixCursor(projection);
-        Object[] row = new Object[projection.length];
-        // The only columns we care about is the id
-        row[SUMMARY_ID_COLUMN_INDEX] = JOIN_MODE_SHOW_ALL_CONTACTS_ID;
-        matrixCursor.addRow(row);
-        return matrixCursor;
-    }
-
-    /**
-     * Calls the currently selected list item.
-     * @return true if the call was initiated, false otherwise
-     */
-    boolean callSelection() {
-        ListView list = getListView();
-        if (list.hasFocus()) {
-            Cursor cursor = (Cursor) list.getSelectedItem();
-            return callContact(cursor);
-        }
-        return false;
-    }
-
-    boolean callContact(Cursor cursor) {
-        return callOrSmsContact(cursor, false /*call*/);
-    }
-
-    boolean smsContact(Cursor cursor) {
-        return callOrSmsContact(cursor, true /*sms*/);
-    }
-
-    /**
-     * Calls the contact which the cursor is point to.
-     * @return true if the call was initiated, false otherwise
-     */
-    boolean callOrSmsContact(Cursor cursor, boolean sendSms) {
-        if (cursor == null) {
-            return false;
-        }
-
-        switch (mMode) {
-            case MODE_PICK_PHONE:
-            case MODE_LEGACY_PICK_PHONE:
-            case MODE_QUERY_PICK_PHONE: {
-                String phone = cursor.getString(PHONE_NUMBER_COLUMN_INDEX);
-                if (sendSms) {
-                    ContactsUtils.initiateSms(this, phone);
-                } else {
-                    ContactsUtils.initiateCall(this, phone);
-                }
-                return true;
-            }
-
-            case MODE_PICK_POSTAL:
-            case MODE_LEGACY_PICK_POSTAL: {
-                return false;
-            }
-
-            default: {
-
-                boolean hasPhone = cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0;
-                if (!hasPhone) {
-                    // There is no phone number.
-                    signalError();
-                    return false;
-                }
-
-                String phone = null;
-                Cursor phonesCursor = null;
-                phonesCursor = queryPhoneNumbers(cursor.getLong(SUMMARY_ID_COLUMN_INDEX));
-                if (phonesCursor == null || phonesCursor.getCount() == 0) {
-                    // No valid number
-                    signalError();
-                    return false;
-                } else if (phonesCursor.getCount() == 1) {
-                    // only one number, call it.
-                    phone = phonesCursor.getString(phonesCursor.getColumnIndex(Phone.NUMBER));
-                } else {
-                    phonesCursor.moveToPosition(-1);
-                    while (phonesCursor.moveToNext()) {
-                        if (phonesCursor.getInt(phonesCursor.
-                                getColumnIndex(Phone.IS_SUPER_PRIMARY)) != 0) {
-                            // Found super primary, call it.
-                            phone = phonesCursor.
-                            getString(phonesCursor.getColumnIndex(Phone.NUMBER));
-                            break;
-                        }
-                    }
-                }
-
-                if (phone == null) {
-                    // Display dialog to choose a number to call.
-                    PhoneDisambigDialog phoneDialog = new PhoneDisambigDialog(
-                            this, phonesCursor, sendSms);
-                    phoneDialog.show();
-                } else {
-                    if (sendSms) {
-                        ContactsUtils.initiateSms(this, phone);
-                    } else {
-                        ContactsUtils.initiateCall(this, phone);
-                    }
-                }
-            }
-        }
-        return true;
-    }
-
-    private Cursor queryPhoneNumbers(long contactId) {
-        Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
-        Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY);
-
-        Cursor c = getContentResolver().query(dataUri,
-                new String[] {Phone._ID, Phone.NUMBER, Phone.IS_SUPER_PRIMARY,
-                        RawContacts.ACCOUNT_TYPE, Phone.TYPE, Phone.LABEL},
-                Data.MIMETYPE + "=?", new String[] {Phone.CONTENT_ITEM_TYPE}, null);
-        if (c != null && c.moveToFirst()) {
-            return c;
-        }
-        return null;
-    }
-
-    // TODO: fix PluralRules to handle zero correctly and use Resources.getQuantityText directly
-    protected String getQuantityText(int count, int zeroResourceId, int pluralResourceId) {
-        if (count == 0) {
-            return getString(zeroResourceId);
-        } else {
-            String format = getResources().getQuantityText(pluralResourceId, count).toString();
-            return String.format(format, count);
-        }
-    }
-
-    /**
-     * Signal an error to the user.
-     */
-    void signalError() {
-        //TODO play an error beep or something...
-    }
-
-    Cursor getItemForView(View view) {
-        ListView listView = getListView();
-        int index = listView.getPositionForView(view);
-        if (index < 0) {
-            return null;
-        }
-        return (Cursor) listView.getAdapter().getItem(index);
-    }
-
-    private static class QueryHandler extends AsyncQueryHandler {
-        protected final WeakReference<ContactsListActivity> mActivity;
-        protected boolean mLoadingJoinSuggestions = false;
-
-        public QueryHandler(Context context) {
-            super(context.getContentResolver());
-            mActivity = new WeakReference<ContactsListActivity>((ContactsListActivity) context);
-        }
-
-        public void setLoadingJoinSuggestions(boolean flag) {
-            mLoadingJoinSuggestions = flag;
-        }
-
-        @Override
-        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
-            final ContactsListActivity activity = mActivity.get();
-            if (activity != null && !activity.isFinishing()) {
-
-                // Whenever we get a suggestions cursor, we need to immediately kick off
-                // another query for the complete list of contacts
-                if (cursor != null && mLoadingJoinSuggestions) {
-                    mLoadingJoinSuggestions = false;
-                    if (cursor.getCount() > 0) {
-                        activity.mAdapter.setSuggestionsCursor(cursor);
-                    } else {
-                        cursor.close();
-                        activity.mAdapter.setSuggestionsCursor(null);
-                    }
-
-                    if (activity.mAdapter.mSuggestionsCursorCount == 0
-                            || !activity.mJoinModeShowAllContacts) {
-                        startQuery(QUERY_TOKEN, null, activity.getContactFilterUri(
-                                        activity.getTextFilter()),
-                                CONTACTS_SUMMARY_PROJECTION,
-                                Contacts._ID + " != " + activity.mQueryAggregateId
-                                        + " AND " + CLAUSE_ONLY_VISIBLE, null,
-                                activity.getSortOrder(CONTACTS_SUMMARY_PROJECTION));
-                        return;
-                    }
-
-                    cursor = activity.getShowAllContactsLabelCursor(CONTACTS_SUMMARY_PROJECTION);
-                }
-
-                activity.mAdapter.changeCursor(cursor);
-
-                // Now that the cursor is populated again, it's possible to restore the list state
-                if (activity.mListState != null) {
-                    activity.mList.onRestoreInstanceState(activity.mListState);
-                    activity.mListState = null;
-                }
-            } else {
-                if (cursor != null) {
-                    cursor.close();
-                }
-            }
-        }
-    }
-
-    final static class ContactListItemCache {
-        public CharArrayBuffer nameBuffer = new CharArrayBuffer(128);
-        public CharArrayBuffer dataBuffer = new CharArrayBuffer(128);
-        public CharArrayBuffer highlightedTextBuffer = new CharArrayBuffer(128);
-        public TextWithHighlighting textWithHighlighting;
-        public CharArrayBuffer phoneticNameBuffer = new CharArrayBuffer(128);
-    }
-
-    final static class PinnedHeaderCache {
-        public TextView titleView;
-        public ColorStateList textColor;
-        public Drawable background;
-    }
-
-    private final class ContactItemListAdapter extends CursorAdapter
-            implements SectionIndexer, OnScrollListener, PinnedHeaderListView.PinnedHeaderAdapter {
-        private SectionIndexer mIndexer;
-        private boolean mLoading = true;
-        private CharSequence mUnknownNameText;
-        private boolean mDisplayPhotos = false;
-        private boolean mDisplayCallButton = false;
-        private boolean mDisplayAdditionalData = true;
-        private int mFrequentSeparatorPos = ListView.INVALID_POSITION;
-        private boolean mDisplaySectionHeaders = true;
-        private Cursor mSuggestionsCursor;
-        private int mSuggestionsCursorCount;
-
-        public ContactItemListAdapter(Context context) {
-            super(context, null, false);
-
-            mUnknownNameText = context.getText(android.R.string.unknownName);
-            switch (mMode) {
-                case MODE_LEGACY_PICK_POSTAL:
-                case MODE_PICK_POSTAL:
-                case MODE_LEGACY_PICK_PHONE:
-                case MODE_PICK_PHONE:
-                case MODE_STREQUENT:
-                case MODE_FREQUENT:
-                    mDisplaySectionHeaders = false;
-                    break;
-            }
-
-            if (mSearchMode) {
-                mDisplaySectionHeaders = false;
-            }
-
-            // Do not display the second line of text if in a specific SEARCH query mode, usually for
-            // matching a specific E-mail or phone number. Any contact details
-            // shown would be identical, and columns might not even be present
-            // in the returned cursor.
-            if (mMode != MODE_QUERY_PICK_PHONE && mQueryMode != QUERY_MODE_NONE) {
-                mDisplayAdditionalData = false;
-            }
-
-            if ((mMode & MODE_MASK_NO_DATA) == MODE_MASK_NO_DATA) {
-                mDisplayAdditionalData = false;
-            }
-
-            if ((mMode & MODE_MASK_SHOW_CALL_BUTTON) == MODE_MASK_SHOW_CALL_BUTTON) {
-                mDisplayCallButton = true;
-            }
-
-            if ((mMode & MODE_MASK_SHOW_PHOTOS) == MODE_MASK_SHOW_PHOTOS) {
-                mDisplayPhotos = true;
-            }
-        }
-
-        public boolean getDisplaySectionHeadersEnabled() {
-            return mDisplaySectionHeaders;
-        }
-
-        public void setSuggestionsCursor(Cursor cursor) {
-            if (mSuggestionsCursor != null) {
-                mSuggestionsCursor.close();
-            }
-            mSuggestionsCursor = cursor;
-            mSuggestionsCursorCount = cursor == null ? 0 : cursor.getCount();
-        }
-
-        /**
-         * Callback on the UI thread when the content observer on the backing cursor fires.
-         * Instead of calling requery we need to do an async query so that the requery doesn't
-         * block the UI thread for a long time.
-         */
-        @Override
-        protected void onContentChanged() {
-            CharSequence constraint = getTextFilter();
-            if (!TextUtils.isEmpty(constraint)) {
-                // Reset the filter state then start an async filter operation
-                Filter filter = getFilter();
-                filter.filter(constraint);
-            } else {
-                // Start an async query
-                startQuery();
-            }
-        }
-
-        public void setLoading(boolean loading) {
-            mLoading = loading;
-        }
-
-        @Override
-        public boolean isEmpty() {
-            if (mProviderStatus != ProviderStatus.STATUS_NORMAL) {
-                return true;
-            }
-
-            if (mSearchMode) {
-                return TextUtils.isEmpty(getTextFilter());
-            } else if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW) {
-                // This mode mask adds a header and we always want it to show up, even
-                // if the list is empty, so always claim the list is not empty.
-                return false;
-            } else {
-                if (mCursor == null || mLoading) {
-                    // We don't want the empty state to show when loading.
-                    return false;
-                } else {
-                    return super.isEmpty();
-                }
-            }
-        }
-
-        @Override
-        public int getItemViewType(int position) {
-            if (position == 0 && (mShowNumberOfContacts || (mMode & MODE_MASK_CREATE_NEW) != 0)) {
-                return IGNORE_ITEM_VIEW_TYPE;
-            }
-
-            if (isShowAllContactsItemPosition(position)) {
-                return IGNORE_ITEM_VIEW_TYPE;
-            }
-
-            if (isSearchAllContactsItemPosition(position)) {
-                return IGNORE_ITEM_VIEW_TYPE;
-            }
-
-            if (getSeparatorId(position) != 0) {
-                // We don't want the separator view to be recycled.
-                return IGNORE_ITEM_VIEW_TYPE;
-            }
-
-            return super.getItemViewType(position);
-        }
-
-        @Override
-        public View getView(int position, View convertView, ViewGroup parent) {
-            if (!mDataValid) {
-                throw new IllegalStateException(
-                        "this should only be called when the cursor is valid");
-            }
-
-            // handle the total contacts item
-            if (position == 0 && mShowNumberOfContacts) {
-                return getTotalContactCountView(parent);
-            }
-
-            if (position == 0 && (mMode & MODE_MASK_CREATE_NEW) != 0) {
-                // Add the header for creating a new contact
-                return getLayoutInflater().inflate(R.layout.create_new_contact, parent, false);
-            }
-
-            if (isShowAllContactsItemPosition(position)) {
-                return getLayoutInflater().
-                        inflate(R.layout.contacts_list_show_all_item, parent, false);
-            }
-
-            if (isSearchAllContactsItemPosition(position)) {
-                return getLayoutInflater().
-                        inflate(R.layout.contacts_list_search_all_item, parent, false);
-            }
-
-            // Handle the separator specially
-            int separatorId = getSeparatorId(position);
-            if (separatorId != 0) {
-                TextView view = (TextView) getLayoutInflater().
-                        inflate(R.layout.list_separator, parent, false);
-                view.setText(separatorId);
-                return view;
-            }
-
-            boolean showingSuggestion;
-            Cursor cursor;
-            if (mSuggestionsCursorCount != 0 && position < mSuggestionsCursorCount + 2) {
-                showingSuggestion = true;
-                cursor = mSuggestionsCursor;
-            } else {
-                showingSuggestion = false;
-                cursor = mCursor;
-            }
-
-            int realPosition = getRealPosition(position);
-            if (!cursor.moveToPosition(realPosition)) {
-                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);
-            bindSectionHeader(v, realPosition, mDisplaySectionHeaders && !showingSuggestion);
-            return v;
-        }
-
-        private View getTotalContactCountView(ViewGroup parent) {
-            final LayoutInflater inflater = getLayoutInflater();
-            View view = inflater.inflate(R.layout.total_contacts, parent, false);
-
-            TextView totalContacts = (TextView) view.findViewById(R.id.totalContactsText);
-
-            String text;
-            int count = getRealCount();
-
-            if (mSearchMode && !TextUtils.isEmpty(getTextFilter())) {
-                text = getQuantityText(count, R.string.listFoundAllContactsZero,
-                        R.plurals.searchFoundContacts);
-            } else {
-                if (mDisplayOnlyPhones) {
-                    text = getQuantityText(count, R.string.listTotalPhoneContactsZero,
-                            R.plurals.listTotalPhoneContacts);
-                } else {
-                    text = getQuantityText(count, R.string.listTotalAllContactsZero,
-                            R.plurals.listTotalAllContacts);
-                }
-            }
-            totalContacts.setText(text);
-            return view;
-        }
-
-        private boolean isShowAllContactsItemPosition(int position) {
-            return mMode == MODE_JOIN_CONTACT && mJoinModeShowAllContacts
-                    && mSuggestionsCursorCount != 0 && position == mSuggestionsCursorCount + 2;
-        }
-
-        private boolean isSearchAllContactsItemPosition(int position) {
-            return mSearchMode && position == getCount() - 1;
-        }
-
-        private int getSeparatorId(int position) {
-            int separatorId = 0;
-            if (position == mFrequentSeparatorPos) {
-                separatorId = R.string.favoritesFrquentSeparator;
-            }
-            if (mSuggestionsCursorCount != 0) {
-                if (position == 0) {
-                    separatorId = R.string.separatorJoinAggregateSuggestions;
-                } else if (position == mSuggestionsCursorCount + 1) {
-                    separatorId = R.string.separatorJoinAggregateAll;
-                }
-            }
-            return separatorId;
-        }
-
-        @Override
-        public View newView(Context context, Cursor cursor, ViewGroup parent) {
-            final ContactListItemView view = new ContactListItemView(context, null);
-            view.setOnCallButtonClickListener(ContactsListActivity.this);
-            view.setTag(new ContactListItemCache());
-            return view;
-        }
-
-        @Override
-        public void bindView(View itemView, Context context, Cursor cursor) {
-            final ContactListItemView view = (ContactListItemView)itemView;
-            final ContactListItemCache cache = (ContactListItemCache) view.getTag();
-
-            int typeColumnIndex;
-            int dataColumnIndex;
-            int labelColumnIndex;
-            int defaultType;
-            int nameColumnIndex;
-            int phoneticNameColumnIndex;
-            boolean displayAdditionalData = mDisplayAdditionalData;
-            boolean highlightingEnabled = false;
-            switch(mMode) {
-                case MODE_PICK_PHONE:
-                case MODE_LEGACY_PICK_PHONE:
-                case MODE_QUERY_PICK_PHONE: {
-                    nameColumnIndex = PHONE_DISPLAY_NAME_COLUMN_INDEX;
-                    phoneticNameColumnIndex = -1;
-                    dataColumnIndex = PHONE_NUMBER_COLUMN_INDEX;
-                    typeColumnIndex = PHONE_TYPE_COLUMN_INDEX;
-                    labelColumnIndex = PHONE_LABEL_COLUMN_INDEX;
-                    defaultType = Phone.TYPE_HOME;
-                    break;
-                }
-                case MODE_PICK_POSTAL:
-                case MODE_LEGACY_PICK_POSTAL: {
-                    nameColumnIndex = POSTAL_DISPLAY_NAME_COLUMN_INDEX;
-                    phoneticNameColumnIndex = -1;
-                    dataColumnIndex = POSTAL_ADDRESS_COLUMN_INDEX;
-                    typeColumnIndex = POSTAL_TYPE_COLUMN_INDEX;
-                    labelColumnIndex = POSTAL_LABEL_COLUMN_INDEX;
-                    defaultType = StructuredPostal.TYPE_HOME;
-                    break;
-                }
-                default: {
-                    nameColumnIndex = getSummaryDisplayNameColumnIndex();
-                    if (mMode == MODE_LEGACY_PICK_PERSON
-                            || mMode == MODE_LEGACY_PICK_OR_CREATE_PERSON) {
-                        phoneticNameColumnIndex = -1;
-                    } else {
-                        phoneticNameColumnIndex = SUMMARY_PHONETIC_NAME_COLUMN_INDEX;
-                    }
-                    dataColumnIndex = -1;
-                    typeColumnIndex = -1;
-                    labelColumnIndex = -1;
-                    defaultType = Phone.TYPE_HOME;
-                    displayAdditionalData = false;
-                    highlightingEnabled = mHighlightWhenScrolling && mMode != MODE_STREQUENT;
-                }
-            }
-
-            // Set the name
-            cursor.copyStringToBuffer(nameColumnIndex, cache.nameBuffer);
-            TextView nameView = view.getNameTextView();
-            int size = cache.nameBuffer.sizeCopied;
-            if (size != 0) {
-                if (highlightingEnabled) {
-                    if (cache.textWithHighlighting == null) {
-                        cache.textWithHighlighting =
-                                mHighlightingAnimation.createTextWithHighlighting();
-                    }
-                    buildDisplayNameWithHighlighting(nameView, cursor, cache.nameBuffer,
-                            cache.highlightedTextBuffer, cache.textWithHighlighting);
-                } else {
-                    nameView.setText(cache.nameBuffer.data, 0, size);
-                }
-            } else {
-                nameView.setText(mUnknownNameText);
-            }
-
-            boolean hasPhone = cursor.getColumnCount() >= SUMMARY_HAS_PHONE_COLUMN_INDEX
-                    && cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0;
-
-            // Make the call button visible if requested.
-            if (mDisplayCallButton && hasPhone) {
-                int pos = cursor.getPosition();
-                view.showCallButton(android.R.id.button1, pos);
-            } else {
-                view.hideCallButton();
-            }
-
-            // Set the photo, if requested
-            if (mDisplayPhotos) {
-                boolean useQuickContact = (mMode & MODE_MASK_DISABLE_QUIKCCONTACT) == 0;
-
-                long photoId = 0;
-                if (!cursor.isNull(SUMMARY_PHOTO_ID_COLUMN_INDEX)) {
-                    photoId = cursor.getLong(SUMMARY_PHOTO_ID_COLUMN_INDEX);
-                }
-
-                ImageView viewToUse;
-                if (useQuickContact) {
-                    // Build soft lookup reference
-                    final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
-                    final String lookupKey = cursor.getString(SUMMARY_LOOKUP_KEY_COLUMN_INDEX);
-                    QuickContactBadge quickContact = view.getQuickContact();
-                    quickContact.assignContactUri(Contacts.getLookupUri(contactId, lookupKey));
-                    viewToUse = quickContact;
-                } else {
-                    viewToUse = view.getPhotoView();
-                }
-
-                final int position = cursor.getPosition();
-                mPhotoLoader.loadPhoto(viewToUse, photoId);
-            }
-
-            if ((mMode & MODE_MASK_NO_PRESENCE) == 0) {
-                // Set the proper icon (star or presence or nothing)
-                int serverStatus;
-                if (!cursor.isNull(SUMMARY_PRESENCE_STATUS_COLUMN_INDEX)) {
-                    serverStatus = cursor.getInt(SUMMARY_PRESENCE_STATUS_COLUMN_INDEX);
-                    Drawable icon = ContactPresenceIconUtil.getPresenceIcon(mContext, serverStatus);
-                    if (icon != null) {
-                        view.setPresence(icon);
-                    } else {
-                        view.setPresence(null);
-                    }
-                } else {
-                    view.setPresence(null);
-                }
-            } else {
-                view.setPresence(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)) {
-                        view.setSnippet(email);
-                        showSnippet = true;
-                    }
-                } else if (Organization.CONTENT_ITEM_TYPE.equals(snippetMimeType)) {
-                    String company = cursor.getString(SUMMARY_SNIPPET_DATA1_COLUMN_INDEX);
-                    String title = cursor.getString(SUMMARY_SNIPPET_DATA4_COLUMN_INDEX);
-                    if (!TextUtils.isEmpty(company)) {
-                        if (!TextUtils.isEmpty(title)) {
-                            view.setSnippet(company + " / " + title);
-                        } else {
-                            view.setSnippet(company);
-                        }
-                        showSnippet = true;
-                    } else if (!TextUtils.isEmpty(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)) {
-                        view.setSnippet(nickname);
-                        showSnippet = true;
-                    }
-                }
-
-                if (!showSnippet) {
-                    view.setSnippet(null);
-                }
-            }
-
-            if (!displayAdditionalData) {
-                if (phoneticNameColumnIndex != -1) {
-
-                    // Set the name
-                    cursor.copyStringToBuffer(phoneticNameColumnIndex, cache.phoneticNameBuffer);
-                    int phoneticNameSize = cache.phoneticNameBuffer.sizeCopied;
-                    if (phoneticNameSize != 0) {
-                        view.setLabel(cache.phoneticNameBuffer.data, phoneticNameSize);
-                    } else {
-                        view.setLabel(null);
-                    }
-                } else {
-                    view.setLabel(null);
-                }
-                return;
-            }
-
-            // Set the data.
-            cursor.copyStringToBuffer(dataColumnIndex, cache.dataBuffer);
-
-            size = cache.dataBuffer.sizeCopied;
-            view.setData(cache.dataBuffer.data, size);
-
-            // Set the label.
-            if (!cursor.isNull(typeColumnIndex)) {
-                final int type = cursor.getInt(typeColumnIndex);
-                final String label = cursor.getString(labelColumnIndex);
-
-                if (mMode == MODE_LEGACY_PICK_POSTAL || mMode == MODE_PICK_POSTAL) {
-                    // TODO cache
-                    view.setLabel(StructuredPostal.getTypeLabel(context.getResources(), type,
-                            label));
-                } else {
-                    // TODO cache
-                    view.setLabel(Phone.getTypeLabel(context.getResources(), type, label));
-                }
-            } else {
-                view.setLabel(null);
-            }
-        }
-
-        /**
-         * Computes the span of the display name that has highlighted parts and configures
-         * the display name text view accordingly.
-         */
-        private void buildDisplayNameWithHighlighting(TextView textView, Cursor cursor,
-                CharArrayBuffer buffer1, CharArrayBuffer buffer2,
-                TextWithHighlighting textWithHighlighting) {
-            int oppositeDisplayOrderColumnIndex;
-            if (mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
-                oppositeDisplayOrderColumnIndex = SUMMARY_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX;
-            } else {
-                oppositeDisplayOrderColumnIndex = SUMMARY_DISPLAY_NAME_PRIMARY_COLUMN_INDEX;
-            }
-            cursor.copyStringToBuffer(oppositeDisplayOrderColumnIndex, buffer2);
-
-            textWithHighlighting.setText(buffer1, buffer2);
-            textView.setText(textWithHighlighting);
-        }
-
-        private void bindSectionHeader(View itemView, int position, boolean displaySectionHeaders) {
-            final ContactListItemView view = (ContactListItemView)itemView;
-            final ContactListItemCache cache = (ContactListItemCache) view.getTag();
-            if (!displaySectionHeaders) {
-                view.setSectionHeader(null);
-                view.setDividerVisible(true);
-            } else {
-                final int section = getSectionForPosition(position);
-                if (getPositionForSection(section) == position) {
-                    String title = (String)mIndexer.getSections()[section];
-                    view.setSectionHeader(title);
-                } else {
-                    view.setDividerVisible(false);
-                    view.setSectionHeader(null);
-                }
-
-                // move the divider for the last item in a section
-                if (getPositionForSection(section + 1) - 1 == position) {
-                    view.setDividerVisible(false);
-                } else {
-                    view.setDividerVisible(true);
-                }
-            }
-        }
-
-        @Override
-        public void changeCursor(Cursor cursor) {
-            if (cursor != null) {
-                setLoading(false);
-            }
-
-            // Get the split between starred and frequent items, if the mode is strequent
-            mFrequentSeparatorPos = ListView.INVALID_POSITION;
-            int cursorCount = 0;
-            if (cursor != null && (cursorCount = cursor.getCount()) > 0
-                    && mMode == MODE_STREQUENT) {
-                cursor.move(-1);
-                for (int i = 0; cursor.moveToNext(); i++) {
-                    int starred = cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX);
-                    if (starred == 0) {
-                        if (i > 0) {
-                            // Only add the separator when there are starred items present
-                            mFrequentSeparatorPos = i;
-                        }
-                        break;
-                    }
-                }
-            }
-
-            if (cursor != null && mSearchResultsMode) {
-                TextView foundContactsText = (TextView)findViewById(R.id.search_results_found);
-                String text = getQuantityText(cursor.getCount(), R.string.listFoundAllContactsZero,
-                        R.plurals.listFoundAllContacts);
-                foundContactsText.setText(text);
-            }
-
-            super.changeCursor(cursor);
-            // Update the indexer for the fast scroll widget
-            updateIndexer(cursor);
-        }
-
-        private void updateIndexer(Cursor cursor) {
-            if (cursor == null) {
-                mIndexer = null;
-                return;
-            }
-
-            Bundle bundle = cursor.getExtras();
-            if (bundle.containsKey(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES)) {
-                String sections[] =
-                    bundle.getStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES);
-                int counts[] = bundle.getIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS);
-                mIndexer = new ContactsSectionIndexer(sections, counts);
-            } else {
-                mIndexer = null;
-            }
-        }
-
-        /**
-         * Run the query on a helper thread. Beware that this code does not run
-         * on the main UI thread!
-         */
-        @Override
-        public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
-            return doFilter(constraint.toString());
-        }
-
-        public Object [] getSections() {
-            if (mIndexer == null) {
-                return new String[] { " " };
-            } else {
-                return mIndexer.getSections();
-            }
-        }
-
-        public int getPositionForSection(int sectionIndex) {
-            if (mIndexer == null) {
-                return -1;
-            }
-
-            return mIndexer.getPositionForSection(sectionIndex);
-        }
-
-        public int getSectionForPosition(int position) {
-            if (mIndexer == null) {
-                return -1;
-            }
-
-            return mIndexer.getSectionForPosition(position);
-        }
-
-        @Override
-        public boolean areAllItemsEnabled() {
-            return mMode != MODE_STARRED
-                && !mShowNumberOfContacts
-                && mSuggestionsCursorCount == 0;
-        }
-
-        @Override
-        public boolean isEnabled(int position) {
-            if (mShowNumberOfContacts) {
-                if (position == 0) {
-                    return false;
-                }
-                position--;
-            }
-
-            if (mSuggestionsCursorCount > 0) {
-                return position != 0 && position != mSuggestionsCursorCount + 1;
-            }
-            return position != mFrequentSeparatorPos;
-        }
-
-        @Override
-        public int getCount() {
-            if (!mDataValid) {
-                return 0;
-            }
-            int superCount = super.getCount();
-
-            if (mShowNumberOfContacts && (mSearchMode || superCount > 0)) {
-                // We don't want to count this header if it's the only thing visible, so that
-                // the empty text will display.
-                superCount++;
-            }
-
-            if (mSearchMode) {
-                // Last element in the list is the "Find
-                superCount++;
-            }
-
-            // We do not show the "Create New" button in Search mode
-            if ((mMode & MODE_MASK_CREATE_NEW) != 0 && !mSearchMode) {
-                // Count the "Create new contact" line
-                superCount++;
-            }
-
-            if (mSuggestionsCursorCount != 0) {
-                // When showing suggestions, we have 2 additional list items: the "Suggestions"
-                // and "All contacts" headers.
-                return mSuggestionsCursorCount + superCount + 2;
-            }
-            else if (mFrequentSeparatorPos != ListView.INVALID_POSITION) {
-                // When showing strequent list, we have an additional list item - the separator.
-                return superCount + 1;
-            } else {
-                return superCount;
-            }
-        }
-
-        /**
-         * Gets the actual count of contacts and excludes all the headers.
-         */
-        public int getRealCount() {
-            return super.getCount();
-        }
-
-        private int getRealPosition(int pos) {
-            if (mShowNumberOfContacts) {
-                pos--;
-            }
-
-            if ((mMode & MODE_MASK_CREATE_NEW) != 0 && !mSearchMode) {
-                return pos - 1;
-            } else if (mSuggestionsCursorCount != 0) {
-                // When showing suggestions, we have 2 additional list items: the "Suggestions"
-                // and "All contacts" separators.
-                if (pos < mSuggestionsCursorCount + 2) {
-                    // We are in the upper partition (Suggestions). Adjusting for the "Suggestions"
-                    // separator.
-                    return pos - 1;
-                } else {
-                    // We are in the lower partition (All contacts). Adjusting for the size
-                    // of the upper partition plus the two separators.
-                    return pos - mSuggestionsCursorCount - 2;
-                }
-            } else if (mFrequentSeparatorPos == ListView.INVALID_POSITION) {
-                // No separator, identity map
-                return pos;
-            } else if (pos <= mFrequentSeparatorPos) {
-                // Before or at the separator, identity map
-                return pos;
-            } else {
-                // After the separator, remove 1 from the pos to get the real underlying pos
-                return pos - 1;
-            }
-        }
-
-        @Override
-        public Object getItem(int pos) {
-            if (mSuggestionsCursorCount != 0 && pos <= mSuggestionsCursorCount) {
-                mSuggestionsCursor.moveToPosition(getRealPosition(pos));
-                return mSuggestionsCursor;
-            } else if (isSearchAllContactsItemPosition(pos)){
-                return null;
-            } else {
-                int realPosition = getRealPosition(pos);
-                if (realPosition < 0) {
-                    return null;
-                }
-                return super.getItem(realPosition);
-            }
-        }
-
-        @Override
-        public long getItemId(int pos) {
-            if (mSuggestionsCursorCount != 0 && pos < mSuggestionsCursorCount + 2) {
-                if (mSuggestionsCursor.moveToPosition(pos - 1)) {
-                    return mSuggestionsCursor.getLong(mRowIDColumn);
-                } else {
-                    return 0;
-                }
-            } else if (isSearchAllContactsItemPosition(pos)) {
-                return 0;
-            }
-            int realPosition = getRealPosition(pos);
-            if (realPosition < 0) {
-                return 0;
-            }
-            return super.getItemId(realPosition);
-        }
-
-        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
-                int totalItemCount) {
-            if (view instanceof PinnedHeaderListView) {
-                ((PinnedHeaderListView)view).configureHeaderView(firstVisibleItem);
-            }
-        }
-
-        public void onScrollStateChanged(AbsListView view, int scrollState) {
-            if (mHighlightWhenScrolling) {
-                if (scrollState != OnScrollListener.SCROLL_STATE_IDLE) {
-                    mHighlightingAnimation.startHighlighting();
-                } else {
-                    mHighlightingAnimation.stopHighlighting();
-                }
-            }
-
-            if (scrollState == OnScrollListener.SCROLL_STATE_FLING) {
-                mPhotoLoader.pause();
-            } else if (mDisplayPhotos) {
-                mPhotoLoader.resume();
-            }
-        }
-
-        /**
-         * Computes the state of the pinned header.  It can be invisible, fully
-         * visible or partially pushed up out of the view.
-         */
-        public int getPinnedHeaderState(int position) {
-            if (mIndexer == null || mCursor == null || mCursor.getCount() == 0) {
-                return PINNED_HEADER_GONE;
-            }
-
-            int realPosition = getRealPosition(position);
-            if (realPosition < 0) {
-                return PINNED_HEADER_GONE;
-            }
-
-            // The header should get pushed up if the top item shown
-            // is the last item in a section for a particular letter.
-            int section = getSectionForPosition(realPosition);
-            int nextSectionPosition = getPositionForSection(section + 1);
-            if (nextSectionPosition != -1 && realPosition == nextSectionPosition - 1) {
-                return PINNED_HEADER_PUSHED_UP;
-            }
-
-            return PINNED_HEADER_VISIBLE;
-        }
-
-        /**
-         * Configures the pinned header by setting the appropriate text label
-         * and also adjusting color if necessary.  The color needs to be
-         * adjusted when the pinned header is being pushed up from the view.
-         */
-        public void configurePinnedHeader(View header, int position, int alpha) {
-            PinnedHeaderCache cache = (PinnedHeaderCache)header.getTag();
-            if (cache == null) {
-                cache = new PinnedHeaderCache();
-                cache.titleView = (TextView)header.findViewById(R.id.header_text);
-                cache.textColor = cache.titleView.getTextColors();
-                cache.background = header.getBackground();
-                header.setTag(cache);
-            }
-
-            int realPosition = getRealPosition(position);
-            int section = getSectionForPosition(realPosition);
-
-            String title = (String)mIndexer.getSections()[section];
-            cache.titleView.setText(title);
-
-            if (alpha == 255) {
-                // Opaque: use the default background, and the original text color
-                header.setBackgroundDrawable(cache.background);
-                cache.titleView.setTextColor(cache.textColor);
-            } else {
-                // Faded: use a solid color approximation of the background, and
-                // a translucent text color
-                header.setBackgroundColor(Color.rgb(
-                        Color.red(mPinnedHeaderBackgroundColor) * alpha / 255,
-                        Color.green(mPinnedHeaderBackgroundColor) * alpha / 255,
-                        Color.blue(mPinnedHeaderBackgroundColor) * alpha / 255));
-
-                int textColor = cache.textColor.getDefaultColor();
-                cache.titleView.setTextColor(Color.argb(alpha,
-                        Color.red(textColor), Color.green(textColor), Color.blue(textColor)));
-            }
-        }
+        return mCallOrSmsInitiator;
     }
 }
diff --git a/src/com/android/contacts/ContactsSearchActivity.java b/src/com/android/contacts/ContactsSearchActivity.java
new file mode 100644
index 0000000..3f1e62c
--- /dev/null
+++ b/src/com/android/contacts/ContactsSearchActivity.java
@@ -0,0 +1,21 @@
+/*
+ * 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;
+
+public class ContactsSearchActivity extends ContactsListActivity {
+
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/ContactsSearchManager.java b/src/com/android/contacts/ContactsSearchManager.java
index d65e079..340d7d6 100644
--- a/src/com/android/contacts/ContactsSearchManager.java
+++ b/src/com/android/contacts/ContactsSearchManager.java
@@ -16,6 +16,8 @@
 
 package com.android.contacts;
 
+import com.android.contacts.list.ContactsRequest;
+
 import android.app.Activity;
 import android.content.Intent;
 import android.os.Bundle;
@@ -40,18 +42,39 @@
     public static final String ORIGINAL_COMPONENT_EXTRA_KEY = "originalComponent";
 
     /**
+     * An extra that provides context for search UI and defines the scope for
+     * the search queries.
+     */
+    public static final String ORIGINAL_TYPE_EXTRA_KEY = "originalType";
+
+    /**
+     * An extra that provides context for search UI and defines the scope for
+     * the search queries.
+     */
+    public static final String ORIGINAL_ACTION_CODE_EXTRA_KEY = "originalActionCode";
+
+    public static final String ORIGINAL_REQUEST_KEY = "originalRequest";
+
+    /**
      * Starts the contact list activity in the search mode.
      */
     public static void startSearch(Activity context, String initialQuery) {
-        context.startActivity(buildIntent(context, initialQuery));
+        context.startActivity(buildIntent(context, initialQuery, null));
     }
 
     public static void startSearchForResult(Activity context, String initialQuery,
-            int requestCode) {
-        context.startActivityForResult(buildIntent(context, initialQuery), requestCode);
+            int requestCode, ContactsRequest originalRequest) {
+        context.startActivityForResult(
+                buildIntent(context, initialQuery, originalRequest), requestCode);
     }
 
-    private static Intent buildIntent(Activity context, String initialQuery) {
+    public static void startSearch(Activity context, String initialQuery,
+            ContactsRequest originalRequest) {
+        context.startActivity(buildIntent(context, initialQuery, originalRequest));
+    }
+
+    private static Intent buildIntent(
+            Activity context, String initialQuery, ContactsRequest originalRequest) {
         Intent intent = new Intent();
         intent.setData(ContactsContract.Contacts.CONTENT_URI);
         intent.setAction(UI.FILTER_CONTACTS_ACTION);
@@ -62,8 +85,9 @@
             intent.putExtras(originalExtras);
         }
         intent.putExtra(UI.FILTER_TEXT_EXTRA_KEY, initialQuery);
-        intent.putExtra(ORIGINAL_ACTION_EXTRA_KEY, originalIntent.getAction());
-        intent.putExtra(ORIGINAL_COMPONENT_EXTRA_KEY, originalIntent.getComponent().getClassName());
+        if (originalRequest != null) {
+            intent.putExtra(ORIGINAL_REQUEST_KEY, originalRequest);
+        }
         return intent;
     }
 }
diff --git a/src/com/android/contacts/ExportVCardActivity.java b/src/com/android/contacts/ExportVCardActivity.java
index 5bccc7a..a7a3ba2 100644
--- a/src/com/android/contacts/ExportVCardActivity.java
+++ b/src/com/android/contacts/ExportVCardActivity.java
@@ -26,6 +26,7 @@
 import android.os.Handler;
 import android.os.PowerManager;
 import android.pim.vcard.VCardComposer;
+import android.pim.vcard.VCardConfig;
 import android.text.TextUtils;
 import android.util.Log;
 
@@ -146,7 +147,8 @@
                     return;
                 }
 
-                composer = new VCardComposer(ExportVCardActivity.this, mVCardTypeStr, true);
+                final int vcardType = VCardConfig.getVCardTypeFromString(mVCardTypeStr);
+                composer = new VCardComposer(ExportVCardActivity.this, vcardType, true);
                 /*int vcardType = (VCardConfig.VCARD_TYPE_V21_GENERIC |
                         VCardConfig.FLAG_USE_QP_TO_PRIMARY_PROPERTIES);
                 composer = new VCardComposer(ExportVCardActivity.this, vcardType, true);*/
diff --git a/src/com/android/contacts/ImportVCardActivity.java b/src/com/android/contacts/ImportVCardActivity.java
index 0a324fe..7ad9228 100644
--- a/src/com/android/contacts/ImportVCardActivity.java
+++ b/src/com/android/contacts/ImportVCardActivity.java
@@ -21,8 +21,6 @@
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.app.ProgressDialog;
-import android.content.ContentResolver;
-import android.content.ContentUris;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
@@ -33,20 +31,6 @@
 import android.os.Environment;
 import android.os.Handler;
 import android.os.PowerManager;
-import android.pim.vcard.VCardConfig;
-import android.pim.vcard.VCardEntryCommitter;
-import android.pim.vcard.VCardEntryConstructor;
-import android.pim.vcard.VCardEntryCounter;
-import android.pim.vcard.VCardInterpreter;
-import android.pim.vcard.VCardInterpreterCollection;
-import android.pim.vcard.VCardParser_V21;
-import android.pim.vcard.VCardParser_V30;
-import android.pim.vcard.VCardSourceDetector;
-import android.pim.vcard.exception.VCardException;
-import android.pim.vcard.exception.VCardNestedException;
-import android.pim.vcard.exception.VCardNotSupportedException;
-import android.pim.vcard.exception.VCardVersionException;
-import android.provider.ContactsContract.RawContacts;
 import android.text.SpannableStringBuilder;
 import android.text.Spanned;
 import android.text.TextUtils;
@@ -58,45 +42,20 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.io.InputStream;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Locale;
 import java.util.Set;
 import java.util.Vector;
 
-class VCardFile {
-    private String mName;
-    private String mCanonicalPath;
-    private long mLastModified;
 
-    public VCardFile(String name, String canonicalPath, long lastModified) {
-        mName = name;
-        mCanonicalPath = canonicalPath;
-        mLastModified = lastModified;
-    }
-
-    public String getName() {
-        return mName;
-    }
-
-    public String getCanonicalPath() {
-        return mCanonicalPath;
-    }
-
-    public long getLastModified() {
-        return mLastModified;
-    }
-}
 
 /**
- * Class for importing vCard. Several user interaction will be required while reading
- * (selecting a file, waiting a moment, etc.)
+ * The class letting users to import vCard. This includes the UI part for letting them select
+ * an Account and posssibly a file if there's no Uri is given from its caller Activity.
  *
  * Note that this Activity assumes that the instance is a "one-shot Activity", which will be
  * finished (with the method {@link Activity#finish()}) after the import and never reuse
@@ -105,24 +64,51 @@
  */
 public class ImportVCardActivity extends Activity {
     private static final String LOG_TAG = "ImportVCardActivity";
-    private static final boolean DO_PERFORMANCE_PROFILE = false;
+
+    private static final int SELECT_ACCOUNT = 0;
+
+    /* package */ static final String VCARD_URI_ARRAY = "vcard_uri_array";
 
     // Run on the UI thread. Must not be null except after onDestroy().
     private Handler mHandler = new Handler();
 
     private AccountSelectionUtil.AccountSelectedListener mAccountSelectionListener;
-    private Account mAccount;
+    private String mAccountName;
+    private String mAccountType;
+
+    private String mAction;
+    private Uri mUri;
 
     private ProgressDialog mProgressDialogForScanVCard;
 
     private List<VCardFile> mAllVCardFileList;
     private VCardScanThread mVCardScanThread;
-    private VCardReadThread mVCardReadThread;
-    private ProgressDialog mProgressDialogForReadVCard;
 
     private String mErrorMessage;
 
-    private boolean mNeedReview = false;
+    private static class VCardFile {
+        private final String mName;
+        private final String mCanonicalPath;
+        private final long mLastModified;
+
+        public VCardFile(String name, String canonicalPath, long lastModified) {
+            mName = name;
+            mCanonicalPath = canonicalPath;
+            mLastModified = lastModified;
+        }
+
+        public String getName() {
+            return mName;
+        }
+
+        public String getCanonicalPath() {
+            return mCanonicalPath;
+        }
+
+        public long getLastModified() {
+            return mLastModified;
+        }
+    }
 
     // Runs on the UI thread.
     private class DialogDisplayer implements Runnable {
@@ -152,300 +138,6 @@
 
     private CancelListener mCancelListener = new CancelListener();
 
-    private class VCardReadThread extends Thread
-            implements DialogInterface.OnCancelListener {
-        private ContentResolver mResolver;
-        private VCardParser_V21 mVCardParser;
-        private boolean mCanceled;
-        private PowerManager.WakeLock mWakeLock;
-        private Uri mUri;
-        private File mTempFile;
-
-        private List<VCardFile> mSelectedVCardFileList;
-        private List<String> mErrorFileNameList;
-
-        public VCardReadThread(Uri uri) {
-            mUri = uri;
-            init();
-        }
-
-        public VCardReadThread(final List<VCardFile> selectedVCardFileList) {
-            mSelectedVCardFileList = selectedVCardFileList;
-            mErrorFileNameList = new ArrayList<String>();
-            init();
-        }
-
-        private void init() {
-            Context context = ImportVCardActivity.this;
-            mResolver = context.getContentResolver();
-            PowerManager powerManager = (PowerManager)context.getSystemService(
-                    Context.POWER_SERVICE);
-            mWakeLock = powerManager.newWakeLock(
-                    PowerManager.SCREEN_DIM_WAKE_LOCK |
-                    PowerManager.ON_AFTER_RELEASE, LOG_TAG);
-        }
-
-        @Override
-        public void finalize() {
-            if (mWakeLock != null && mWakeLock.isHeld()) {
-                mWakeLock.release();
-            }
-        }
-
-        @Override
-        public void run() {
-            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 targetUri = mUri;
-                    mProgressDialogForReadVCard.setProgressNumberFormat("");
-                    mProgressDialogForReadVCard.setProgress(0);
-
-                    // Count the number of VCard entries
-                    mProgressDialogForReadVCard.setIndeterminate(true);
-                    long start;
-                    if (DO_PERFORMANCE_PROFILE) {
-                        start = System.currentTimeMillis();
-                    }
-                    VCardEntryCounter counter = new VCardEntryCounter();
-                    VCardSourceDetector detector = new VCardSourceDetector();
-                    VCardInterpreterCollection builderCollection = new VCardInterpreterCollection(
-                            Arrays.asList(counter, detector));
-                    boolean result;
-                    try {
-                        result = readOneVCardFile(targetUri,
-                                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(targetUri,
-                                    VCardConfig.DEFAULT_CHARSET, counter, detector, false, null);
-                        } catch (VCardNestedException e2) {
-                            result = false;
-                            Log.e(LOG_TAG, "Must not reach here. " + e2);
-                        }
-                    }
-                    if (DO_PERFORMANCE_PROFILE) {
-                        long time = System.currentTimeMillis() - start;
-                        Log.d(LOG_TAG, "time for counting the number of vCard entries: " +
-                                time + " ms");
-                    }
-                    if (!result) {
-                        shouldCallFinish = false;
-                        return;
-                    }
-
-                    mProgressDialogForReadVCard.setProgressNumberFormat(
-                            getString(R.string.reading_vcard_contacts));
-                    mProgressDialogForReadVCard.setIndeterminate(false);
-                    mProgressDialogForReadVCard.setMax(counter.getCount());
-                    String charset = detector.getEstimatedCharset();
-                    createdUri = doActuallyReadOneVCard(targetUri, null, charset, true, detector,
-                            mErrorFileNameList);
-                } else {  // Read multiple files.
-                    mProgressDialogForReadVCard.setProgressNumberFormat(
-                            getString(R.string.reading_vcard_files));
-                    mProgressDialogForReadVCard.setMax(mSelectedVCardFileList.size());
-                    mProgressDialogForReadVCard.setProgress(0);
-
-                    for (VCardFile vcardFile : mSelectedVCardFileList) {
-                        if (mCanceled) {
-                            return;
-                        }
-                        // TODO: detect scheme!
-                        final Uri targetUri =
-                            Uri.parse("file://" + vcardFile.getCanonicalPath());
-
-                        VCardSourceDetector detector = new VCardSourceDetector();
-                        try {
-                            if (!readOneVCardFile(targetUri, VCardConfig.DEFAULT_CHARSET,
-                                    detector, null, true, mErrorFileNameList)) {
-                                continue;
-                            }
-                        } catch (VCardNestedException e) {
-                            // Assume that VCardSourceDetector was able to detect the source.
-                        }
-                        String charset = detector.getEstimatedCharset();
-                        doActuallyReadOneVCard(targetUri, mAccount,
-                                charset, false, detector, mErrorFileNameList);
-                        mProgressDialogForReadVCard.incrementProgressBy(1);
-                    }
-                }
-            } 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()) {
-                        finish();
-                        if (mNeedReview) {
-                            mNeedReview = false;
-                            Log.v("importVCardActivity", "Prepare to review the imported contact");
-
-                            if (createdUri != null) {
-                                // get contact_id of this raw_contact
-                                final long rawContactId = ContentUris.parseId(createdUri);
-                                Uri contactUri = RawContacts.getContactLookupUri(
-                                        getContentResolver(), ContentUris.withAppendedId(
-                                                RawContacts.CONTENT_URI, rawContactId));
-
-                                Intent viewIntent = new Intent(Intent.ACTION_VIEW, contactUri);
-                                startActivity(viewIntent);
-                            }
-                        }
-                    } else {
-                        StringBuilder builder = new StringBuilder();
-                        boolean first = true;
-                        for (String fileName : mErrorFileNameList) {
-                            if (first) {
-                                first = false;
-                            } else {
-                                builder.append(", ");
-                            }
-                            builder.append(fileName);
-                        }
-
-                        runOnUIThread(new DialogDisplayer(
-                                getString(R.string.fail_reason_failed_to_read_files,
-                                        builder.toString())));
-                    }
-                }
-            }
-        }
-
-        private Uri doActuallyReadOneVCard(Uri uri, Account account,
-                String charset, boolean showEntryParseProgress,
-                VCardSourceDetector detector, List<String> errorFileNameList) {
-            final Context context = ImportVCardActivity.this;
-            VCardEntryConstructor builder;
-            final String currentLanguage = Locale.getDefault().getLanguage();
-            int vcardType = VCardConfig.getVCardTypeFromString(
-                    context.getString(R.string.config_import_vcard_type));
-            if (charset != null) {
-                builder = new VCardEntryConstructor(charset, charset, false, vcardType, mAccount);
-            } else {
-                charset = VCardConfig.DEFAULT_CHARSET;
-                builder = new VCardEntryConstructor(null, null, false, vcardType, mAccount);
-            }
-            VCardEntryCommitter committer = new VCardEntryCommitter(mResolver);
-            builder.addEntryHandler(committer);
-            if (showEntryParseProgress) {
-                builder.addEntryHandler(new ProgressShower(mProgressDialogForReadVCard,
-                        context.getString(R.string.reading_vcard_message),
-                        ImportVCardActivity.this,
-                        mHandler));
-            }
-
-            try {
-                if (!readOneVCardFile(uri, charset, builder, detector, false, null)) {
-                    return null;
-                }
-            } catch (VCardNestedException e) {
-                Log.e(LOG_TAG, "Never reach here.");
-            }
-            final ArrayList<Uri> createdUris = committer.getCreatedUris();
-            return (createdUris == null || createdUris.size() != 1) ? null : createdUris.get(0);
-        }
-
-        private boolean readOneVCardFile(Uri uri, String charset,
-                VCardInterpreter builder, VCardSourceDetector detector,
-                boolean throwNestedException, List<String> errorFileNameList)
-                throws VCardNestedException {
-            InputStream is;
-            try {
-                is = mResolver.openInputStream(uri);
-                mVCardParser = new VCardParser_V21(detector);
-
-                try {
-                    mVCardParser.parse(is, charset, builder, mCanceled);
-                } catch (VCardVersionException e1) {
-                    try {
-                        is.close();
-                    } catch (IOException e) {
-                    }
-                    if (builder instanceof VCardEntryConstructor) {
-                        // Let the object clean up internal temporal objects,
-                        ((VCardEntryConstructor)builder).clear();
-                    }
-                    is = mResolver.openInputStream(uri);
-
-                    try {
-                        mVCardParser = new VCardParser_V30();
-                        mVCardParser.parse(is, charset, builder, mCanceled);
-                    } catch (VCardVersionException e2) {
-                        throw new VCardException("vCard with unspported version.");
-                    }
-                } finally {
-                    if (is != null) {
-                        try {
-                            is.close();
-                        } catch (IOException e) {
-                        }
-                    }
-                }
-            } catch (IOException e) {
-                Log.e(LOG_TAG, "IOException was emitted: " + e.getMessage());
-
-                mProgressDialogForReadVCard.dismiss();
-
-                if (errorFileNameList != null) {
-                    errorFileNameList.add(uri.toString());
-                } else {
-                    runOnUIThread(new DialogDisplayer(
-                            getString(R.string.fail_reason_io_error) +
-                                    ": " + e.getLocalizedMessage()));
-                }
-                return false;
-            } catch (VCardNotSupportedException e) {
-                if ((e instanceof VCardNestedException) && throwNestedException) {
-                    throw (VCardNestedException)e;
-                }
-                if (errorFileNameList != null) {
-                    errorFileNameList.add(uri.toString());
-                } else {
-                    runOnUIThread(new DialogDisplayer(
-                            getString(R.string.fail_reason_vcard_not_supported_error) +
-                            " (" + e.getMessage() + ")"));
-                }
-                return false;
-            } catch (VCardException e) {
-                if (errorFileNameList != null) {
-                    errorFileNameList.add(uri.toString());
-                } else {
-                    runOnUIThread(new DialogDisplayer(
-                            getString(R.string.fail_reason_vcard_parse_error) +
-                            " (" + e.getMessage() + ")"));
-                }
-                return false;
-            }
-            return true;
-        }
-
-        public void cancel() {
-            mCanceled = true;
-            if (mVCardParser != null) {
-                mVCardParser.cancel();
-            }
-        }
-
-        public void onCancel(DialogInterface dialog) {
-            cancel();
-        }
-    }
-
     private class ImportTypeSelectedListener implements
             DialogInterface.OnClickListener {
         public static final int IMPORT_ONE = 0;
@@ -459,7 +151,7 @@
             if (which == DialogInterface.BUTTON_POSITIVE) {
                 switch (mCurrentIndex) {
                 case IMPORT_ALL:
-                    importMultipleVCardFromSDCard(mAllVCardFileList);
+                    importVCardFromSDCard(mAllVCardFileList);
                     break;
                 case IMPORT_MULTIPLE:
                     showDialog(R.id.dialog_select_multiple_vcard);
@@ -492,18 +184,16 @@
             if (which == DialogInterface.BUTTON_POSITIVE) {
                 if (mSelectedIndexSet != null) {
                     List<VCardFile> selectedVCardFileList = new ArrayList<VCardFile>();
-                    int size = mAllVCardFileList.size();
+                    final int size = mAllVCardFileList.size();
                     // We'd like to sort the files by its index, so we do not use Set iterator.
                     for (int i = 0; i < size; i++) {
                         if (mSelectedIndexSet.contains(i)) {
                             selectedVCardFileList.add(mAllVCardFileList.get(i));
                         }
                     }
-                    importMultipleVCardFromSDCard(selectedVCardFileList);
+                    importVCardFromSDCard(selectedVCardFileList);
                 } else {
-                    String canonicalPath = mAllVCardFileList.get(mCurrentIndex).getCanonicalPath();
-                    final Uri uri = Uri.parse("file://" + canonicalPath);
-                    importOneVCardFromSDCard(uri);
+                    importVCardFromSDCard(mAllVCardFileList.get(mCurrentIndex));
                 }
             } else if (which == DialogInterface.BUTTON_NEGATIVE) {
                 finish();
@@ -642,12 +332,9 @@
 
     private void startVCardSelectAndImport() {
         int size = mAllVCardFileList.size();
-        if (getResources().getBoolean(R.bool.config_import_all_vcard_from_sdcard_automatically)) {
-            importMultipleVCardFromSDCard(mAllVCardFileList);
-        } else if (size == 1) {
-            String canonicalPath = mAllVCardFileList.get(0).getCanonicalPath();
-            Uri uri = Uri.parse("file://" + canonicalPath);
-            importOneVCardFromSDCard(uri);
+        if (getResources().getBoolean(R.bool.config_import_all_vcard_from_sdcard_automatically) ||
+                size == 1) {
+            importVCardFromSDCard(mAllVCardFileList);
         } else if (getResources().getBoolean(R.bool.config_allow_users_select_all_vcard_import)) {
             runOnUIThread(new DialogDisplayer(R.id.dialog_select_import_type));
         } else {
@@ -655,53 +342,70 @@
         }
     }
 
-    private void importMultipleVCardFromSDCard(final List<VCardFile> selectedVCardFileList) {
-        runOnUIThread(new Runnable() {
-            public void run() {
-                mVCardReadThread = new VCardReadThread(selectedVCardFileList);
-                showDialog(R.id.dialog_reading_vcard);
-            }
-        });
+    private void importVCardFromSDCard(final List<VCardFile> selectedVCardFileList) {
+        final int size = selectedVCardFileList.size();
+        String[] uriStrings = new String[size];
+        int i = 0;
+        for (VCardFile vcardFile : selectedVCardFileList) {
+            uriStrings[i] = "file://" + vcardFile.getCanonicalPath();
+            i++;
+        }
+        importVCard(uriStrings);
+    }
+    
+    private void importVCardFromSDCard(final VCardFile vcardFile) {
+        String[] uriStrings = new String[1];
+        uriStrings[0] = "file://" + vcardFile.getCanonicalPath();
+        importVCard(uriStrings);
     }
 
-    private void importOneVCardFromSDCard(final Uri uri) {
-        runOnUIThread(new Runnable() {
-            public void run() {
-                mVCardReadThread = new VCardReadThread(uri);
-                showDialog(R.id.dialog_reading_vcard);
-            }
-        });
+    private void importVCard(final String uriString) {
+        String[] uriStrings = new String[1];
+        uriStrings[0] = uriString;
+        importVCard(uriStrings);
+    }
+
+    private void importVCard(final String[] uriStrings) {
+        final Intent intent = new Intent(this, ImportVCardService.class);
+        intent.putExtra(VCARD_URI_ARRAY, uriStrings);
+        intent.putExtra(SelectAccountActivity.ACCOUNT_NAME, mAccountName);
+        intent.putExtra(SelectAccountActivity.ACCOUNT_TYPE, mAccountType);
+
+        // TODO: permission is not migrated to ImportVCardService, so some exception is
+        // thrown when reading some Uri, permission of which is temporarily guaranteed
+        // to ImportVCardActivity, not ImportVCardService.
+        startService(intent);
+        finish();
     }
 
     private Dialog getSelectImportTypeDialog() {
-        DialogInterface.OnClickListener listener =
-            new ImportTypeSelectedListener();
-        AlertDialog.Builder builder = new AlertDialog.Builder(this)
-            .setTitle(R.string.select_vcard_title)
-            .setPositiveButton(android.R.string.ok, listener)
-            .setOnCancelListener(mCancelListener)
-            .setNegativeButton(android.R.string.cancel, mCancelListener);
+        final DialogInterface.OnClickListener listener = new ImportTypeSelectedListener();
+        final AlertDialog.Builder builder = new AlertDialog.Builder(this)
+                .setTitle(R.string.select_vcard_title)
+                .setPositiveButton(android.R.string.ok, listener)
+                .setOnCancelListener(mCancelListener)
+                .setNegativeButton(android.R.string.cancel, mCancelListener);
 
-        String[] items = new String[ImportTypeSelectedListener.IMPORT_TYPE_SIZE];
+        final String[] items = new String[ImportTypeSelectedListener.IMPORT_TYPE_SIZE];
         items[ImportTypeSelectedListener.IMPORT_ONE] =
-            getString(R.string.import_one_vcard_string);
+                getString(R.string.import_one_vcard_string);
         items[ImportTypeSelectedListener.IMPORT_MULTIPLE] =
-            getString(R.string.import_multiple_vcard_string);
+                getString(R.string.import_multiple_vcard_string);
         items[ImportTypeSelectedListener.IMPORT_ALL] =
-            getString(R.string.import_all_vcard_string);
+                getString(R.string.import_all_vcard_string);
         builder.setSingleChoiceItems(items, ImportTypeSelectedListener.IMPORT_ONE, listener);
         return builder.create();
     }
 
     private Dialog getVCardFileSelectDialog(boolean multipleSelect) {
-        int size = mAllVCardFileList.size();
-        VCardSelectedListener listener = new VCardSelectedListener(multipleSelect);
-        AlertDialog.Builder builder =
-            new AlertDialog.Builder(this)
-                .setTitle(R.string.select_vcard_title)
-                .setPositiveButton(android.R.string.ok, listener)
-                .setOnCancelListener(mCancelListener)
-                .setNegativeButton(android.R.string.cancel, mCancelListener);
+        final int size = mAllVCardFileList.size();
+        final VCardSelectedListener listener = new VCardSelectedListener(multipleSelect);
+        final AlertDialog.Builder builder =
+                new AlertDialog.Builder(this)
+                        .setTitle(R.string.select_vcard_title)
+                        .setPositiveButton(android.R.string.ok, listener)
+                        .setOnCancelListener(mCancelListener)
+                        .setNegativeButton(android.R.string.cancel, mCancelListener);
 
         CharSequence[] items = new CharSequence[size];
         DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@@ -735,67 +439,62 @@
 
         final Intent intent = getIntent();
         if (intent != null) {
-            final String accountName = intent.getStringExtra("account_name");
-            final String accountType = intent.getStringExtra("account_type");
-            if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
-                mAccount = new Account(accountName, accountType);
-            }
+            mAccountName = intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME);
+            mAccountType = intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE);
+            mAction = intent.getAction();
+            mUri = intent.getData();
         } else {
             Log.e(LOG_TAG, "intent does not exist");
         }
 
-        // The caller often does not know account information at all, so we show the UI instead.
-        if (mAccount == null) {
-            // There's three possibilities:
-            // - more than one accounts -> ask the user
-            // - just one account -> use the account without asking the user
-            // - no account -> use phone-local storage without asking the user
+        // The caller may not know account information at all, so we show the UI instead.
+        if (TextUtils.isEmpty(mAccountName) || TextUtils.isEmpty(mAccountType)) {
             final Sources sources = Sources.getInstance(this);
             final List<Account> accountList = sources.getAccounts(true);
-            final int size = accountList.size();
-            if (size > 1) {
-                final int resId = R.string.import_from_sdcard;
-                mAccountSelectionListener =
-                    new AccountSelectionUtil.AccountSelectedListener(
-                            this, accountList, resId) {
-                    @Override
-                    public void onClick(DialogInterface dialog, int which) {
-                        dialog.dismiss();
-                        mAccount = mAccountList.get(which);
-                        // Instead of using Intent mechanism, call the relevant private method,
-                        // to avoid throwing an Intent to itself again.
-                        startImport();
-                    }
-                };
-                showDialog(resId);
-                return;
+            if (accountList.size() == 0) {
+                mAccountName = null;
+                mAccountType = null;
+            } else if (accountList.size() == 1) {
+                final Account account = accountList.get(0);
+                mAccountName = account.name;
+                mAccountType = account.type;
             } else {
-                mAccount = size > 0 ? accountList.get(0) : null;
+                startActivityForResult(new Intent(this, SelectAccountActivity.class),
+                        SELECT_ACCOUNT);
+                return;
             }
         }
-
-        startImport();
+        startImport(mAction, mUri);
     }
 
-    private void startImport() {
-        Intent intent = getIntent();
-        final String action = intent.getAction();
-        final Uri uri = intent.getData();
-        Log.v(LOG_TAG, "action = " + action + " ; path = " + uri);
-        if (Intent.ACTION_VIEW.equals(action)) {
-            // Import the file directly and then go to EDIT screen
-            mNeedReview = true;
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+        if (requestCode == SELECT_ACCOUNT) {
+            if (resultCode == RESULT_OK) {
+                mAccountName = intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME);
+                mAccountType = intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE);
+                startImport(mAction, mUri);
+            } else {
+                if (resultCode != RESULT_CANCELED) {
+                    Log.w(LOG_TAG, "Result code was not OK nor CANCELED: " + resultCode);
+                }
+                finish();
+            }
         }
+    }
+
+    private void startImport(String action, Uri uri) {
+        Log.d(LOG_TAG, "action = " + action + " ; path = " + uri);
 
         if (uri != null) {
-            importOneVCardFromSDCard(uri);
+            importVCard(uri.toString());
         } else {
             doScanExternalStorageAndImportVCard();
         }
     }
 
     @Override
-    protected Dialog onCreateDialog(int resId) {
+    protected Dialog onCreateDialog(int resId, Bundle bundle) {
         switch (resId) {
             case R.string.import_from_sdcard: {
                 if (mAccountSelectionListener == null) {
@@ -803,8 +502,7 @@
                             "mAccountSelectionListener must not be null.");
                 }
                 return AccountSelectionUtil.getSelectAccountDialog(this, resId,
-                        mAccountSelectionListener,
-                        new CancelListener());
+                        mAccountSelectionListener, mCancelListener);
             }
             case R.id.dialog_searching_vcard: {
                 if (mProgressDialogForScanVCard == null) {
@@ -845,19 +543,6 @@
             case R.id.dialog_select_one_vcard: {
                 return getVCardFileSelectDialog(false);
             }
-            case R.id.dialog_reading_vcard: {
-                if (mProgressDialogForReadVCard == null) {
-                    String title = getString(R.string.reading_vcard_title);
-                    String message = getString(R.string.reading_vcard_message);
-                    mProgressDialogForReadVCard = new ProgressDialog(this);
-                    mProgressDialogForReadVCard.setTitle(title);
-                    mProgressDialogForReadVCard.setMessage(message);
-                    mProgressDialogForReadVCard.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
-                    mProgressDialogForReadVCard.setOnCancelListener(mVCardReadThread);
-                    mVCardReadThread.start();
-                }
-                return mProgressDialogForReadVCard;
-            }
             case R.id.dialog_io_exception: {
                 String message = (getString(R.string.scanning_sdcard_failed_message,
                         getString(R.string.fail_reason_io_error)));
@@ -885,17 +570,12 @@
             }
         }
 
-        return super.onCreateDialog(resId);
+        return super.onCreateDialog(resId, bundle);
     }
 
     @Override
     protected void onPause() {
         super.onPause();
-        if (mVCardReadThread != null) {
-            // The Activity is no longer visible. Stop the thread.
-            mVCardReadThread.cancel();
-            mVCardReadThread = null;
-        }
 
         // ImportVCardActivity should not be persistent. In other words, if there's some
         // event calling onPause(), this Activity should finish its work and give the main
@@ -912,29 +592,6 @@
         // make sure that the handler does not run any callback when
         // this activity isFinishing().
 
-        // Need to make sure any worker thread is done before we flush and
-        // nullify the message handler.
-        if (mVCardReadThread != null) {
-            Log.w(LOG_TAG, "VCardReadThread exists while this Activity is now being killed!");
-            mVCardReadThread.cancel();
-            int attempts = 0;
-            while (mVCardReadThread.isAlive() && attempts < 10) {
-                try {
-                    Thread.currentThread().sleep(20);
-                } catch (InterruptedException ie) {
-                    // Keep on going until max attempts is reached.
-                }
-                attempts++;
-            }
-            if (mVCardReadThread.isAlive()) {
-                // Find out why the thread did not exit in a timely
-                // fashion. Last resort: increase the sleep duration
-                // and/or the number of attempts.
-                Log.e(LOG_TAG, "VCardReadThread is still alive after max attempts.");
-            }
-            mVCardReadThread = null;
-        }
-
         // Callbacks messages have what == 0.
         if (mHandler.hasMessages(0)) {
             mHandler.removeMessages(0);
@@ -955,17 +612,6 @@
         }
     }
 
-    @Override
-    public void finalize() {
-        // TODO: This should not be needed. Throw exception instead.
-        if (mVCardReadThread != null) {
-            // Not sure this procedure is really needed, but just in case...
-            Log.e(LOG_TAG, "VCardReadThread exists while this Activity is now being killed!");
-            mVCardReadThread.cancel();
-            mVCardReadThread = null;
-        }
-    }
-
     /**
      * Scans vCard in external storage (typically SDCard) and tries to import it.
      * - When there's no SDCard available, an error dialog is shown.
diff --git a/src/com/android/contacts/ImportVCardService.java b/src/com/android/contacts/ImportVCardService.java
new file mode 100644
index 0000000..8da8651
--- /dev/null
+++ b/src/com/android/contacts/ImportVCardService.java
@@ -0,0 +1,455 @@
+/*
+ * 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 android.accounts.Account;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.IBinder;
+import android.pim.vcard.VCardConfig;
+import android.pim.vcard.VCardEntry;
+import android.pim.vcard.VCardEntryCommitter;
+import android.pim.vcard.VCardEntryConstructor;
+import android.pim.vcard.VCardEntryCounter;
+import android.pim.vcard.VCardEntryHandler;
+import android.pim.vcard.VCardInterpreter;
+import android.pim.vcard.VCardInterpreterCollection;
+import android.pim.vcard.VCardParser;
+import android.pim.vcard.VCardParser_V21;
+import android.pim.vcard.VCardParser_V30;
+import android.pim.vcard.VCardSourceDetector;
+import android.pim.vcard.exception.VCardException;
+import android.pim.vcard.exception.VCardNestedException;
+import android.pim.vcard.exception.VCardNotSupportedException;
+import android.pim.vcard.exception.VCardVersionException;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.RemoteViews;
+import android.widget.Toast;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+
+/**
+ * The class responsible for importing vCard from one ore multiple Uris.
+ */
+public class ImportVCardService extends Service {
+    private final static String LOG_TAG = "ImportVCardService";
+
+    private class ProgressNotifier implements VCardEntryHandler {
+        private final int mId;
+
+        public ProgressNotifier(int id) {
+            mId = id;
+        }
+
+        public void onStart() {
+        }
+
+        public void onEntryCreated(VCardEntry contactStruct) {
+            mCurrentCount++;  // 1 origin.
+            if (contactStruct.isIgnorable()) {
+                return;
+            }
+
+            final Context context = ImportVCardService.this;
+            // We don't use startEntry() since:
+            // - We cannot know name there but here.
+            // - There's high probability where name comes soon after the beginning of entry, so
+            //   we don't need to hurry to show something.
+            final String packageName = "com.android.contacts";
+            final RemoteViews remoteViews = new RemoteViews(packageName,
+                    R.layout.status_bar_ongoing_event_progress_bar);
+            final String title = getString(R.string.reading_vcard_title);
+            final String text = getString(R.string.progress_notifier_message,
+                    String.valueOf(mCurrentCount),
+                    String.valueOf(mTotalCount),
+                    contactStruct.getDisplayName());
+
+            // TODO: uploading image does not work correctly. (looks like a static image).
+            remoteViews.setTextViewText(R.id.description, text);
+            remoteViews.setProgressBar(R.id.progress_bar, mTotalCount, mCurrentCount,
+                    mTotalCount == -1);
+            final String percentage =
+                    getString(R.string.percentage,
+                            String.valueOf(mCurrentCount * 100/mTotalCount));
+            remoteViews.setTextViewText(R.id.progress_text, percentage);
+            remoteViews.setImageViewResource(R.id.appIcon, android.R.drawable.stat_sys_download);
+
+            final Notification notification = new Notification();
+            notification.icon = android.R.drawable.stat_sys_download;
+            notification.flags |= Notification.FLAG_ONGOING_EVENT;
+            notification.contentView = remoteViews;
+
+            notification.contentIntent =
+                    PendingIntent.getActivity(context, 0,
+                            new Intent(context, ContactsListActivity.class), 0);
+            mNotificationManager.notify(mId, notification);
+        }
+
+        public void onEnd() {
+        }
+    }
+
+    private class VCardReadThread extends Thread {
+        private final Context mContext;
+        private final ContentResolver mResolver;
+        private VCardParser mVCardParser;
+        private boolean mCanceled;
+        private final List<Uri> mErrorUris;
+        private final List<Uri> mCreatedUris;
+
+        public VCardReadThread() {
+            mContext = ImportVCardService.this;
+            mResolver = mContext.getContentResolver();
+            mErrorUris = new ArrayList<Uri>();
+            mCreatedUris = new ArrayList<Uri>();
+        }
+
+        @Override
+        public void run() {
+            while (!mCanceled) {
+                mErrorUris.clear();
+                mCreatedUris.clear();
+
+                final Account account;
+                final Uri[] uris;
+                final int id;
+                final boolean needReview;
+                synchronized (mContext) {
+                    if (mPendingInputs.size() == 0) {
+                        mNowRunning = false;
+                        break;
+                    } else {
+                        final PendingInput pendingInput = mPendingInputs.poll();
+                        account = pendingInput.account;
+                        uris = pendingInput.uris;
+                        id = pendingInput.id;
+                    }
+                }
+                runInternal(account, uris, id);
+                doFinishNotification(id, uris);
+            }
+            Log.i(LOG_TAG, "Successfully imported. Total: " + mTotalCount);
+            stopSelf();
+        }
+
+        private void runInternal(Account account, Uri[] uris, int id) {
+            int totalCount = 0;
+            final ArrayList<VCardSourceDetector> detectorList =
+                new ArrayList<VCardSourceDetector>();
+            // First scan all Uris with a default charset and try to understand an exact
+            // charset to be used to each Uri. Note that detector would return null when
+            // it does not know an appropriate charset, so stick to use the default
+            // at that time.
+            // TODO: notification for first scanning?
+            for (Uri uri : uris) {
+                if (mCanceled) {
+                    return;
+                }
+                final VCardEntryCounter counter = new VCardEntryCounter();
+                final VCardSourceDetector detector = new VCardSourceDetector();
+                final VCardInterpreterCollection interpreterCollection =
+                        new VCardInterpreterCollection(Arrays.asList(counter, detector));
+                if (!readOneVCard(uri, VCardConfig.VCARD_TYPE_UNKNOWN, null,
+                        interpreterCollection)) {
+                    mErrorUris.add(uri);
+                }
+
+                totalCount += counter.getCount();
+                detectorList.add(detector);
+            }
+
+            if (mErrorUris.size() > 0) {
+                final StringBuilder builder = new StringBuilder();
+                builder.append("Error happened on ");
+                for (Uri errorUri : mErrorUris) {
+                    builder.append("\"");
+                    builder.append(errorUri.toString());
+                    builder.append("\"");
+                }
+                Log.e(LOG_TAG, builder.toString());
+                doErrorNotification(id);
+                return;
+            }
+
+            if (uris.length != detectorList.size()) {
+                Log.e(LOG_TAG,
+                        "The number of Uris to be imported is different from that of " +
+                        "charset to be used.");
+                doErrorNotification(id);
+                return;
+            }
+
+            // First scanning is over. Try to import each vCard, which causes side effects.
+            mTotalCount = totalCount;
+            mCurrentCount = 0;
+
+            for (int i = 0; i < uris.length; i++) {
+                if (mCanceled) {
+                    Log.w(LOG_TAG, "Canceled during importing (with storing data in database)");
+                    // TODO: implement cancel correctly.
+                    return;
+                }
+                final Uri uri = uris[i];
+
+                final VCardSourceDetector detector = detectorList.get(i);
+                final int vcardType =  detector.getEstimatedType();  
+                final String charset = detector.getEstimatedCharset();  // May be null.
+
+                final VCardEntryConstructor constructor =
+                        new VCardEntryConstructor(vcardType, account, charset);
+                final VCardEntryCommitter committer = new VCardEntryCommitter(mResolver);
+                constructor.addEntryHandler(committer);
+                constructor.addEntryHandler(new ProgressNotifier(id));
+
+                if (!readOneVCard(uri, vcardType, charset, constructor)) {
+                        Log.e(LOG_TAG, "Failed to read \"" + uri.toString() + "\" " +
+                                "while first scan was successful.");
+                }
+                final List<Uri> createdUris = committer.getCreatedUris();
+                if (createdUris != null && createdUris.size() > 0) {
+                    mCreatedUris.addAll(createdUris);
+                } else {
+                    Log.w(LOG_TAG, "Created Uris is null (src = " + uri.toString() + "\"");
+                }
+            }
+        }
+
+        private boolean readOneVCard(Uri uri, int vcardType, String charset,
+                VCardInterpreter interpreter) {
+            InputStream is;
+            try {
+                // TODO: use vcardType given from detector and stop trying to read the file twice.
+                is = mResolver.openInputStream(uri);
+
+                // We need synchronized since we need to handle mCanceled and mVCardParser
+                // at once. In the worst case, a user may call cancel() just before recreating
+                // mVCardParser.
+                synchronized (this) {
+                    mVCardParser = new VCardParser_V21(vcardType, charset);
+                    if (mCanceled) {
+                        mVCardParser.cancel();
+                    }
+                }
+
+                try {
+                    mVCardParser.parse(is, interpreter);
+                } catch (VCardVersionException e1) {
+                    try {
+                        is.close();
+                    } catch (IOException e) {
+                    }
+                    if (interpreter instanceof VCardEntryConstructor) {
+                        // Let the object clean up internal temporal objects,
+                        ((VCardEntryConstructor) interpreter).clear();
+                    }
+                    is = mResolver.openInputStream(uri);
+
+                    synchronized (this) {
+                        mVCardParser = new VCardParser_V30(vcardType, charset);
+                        if (mCanceled) {
+                            mVCardParser.cancel();
+                        }
+                    }
+
+                    try {
+                        mVCardParser.parse(is, interpreter);
+                    } catch (VCardVersionException e2) {
+                        throw new VCardException("vCard with unspported version.");
+                    }
+                } finally {
+                    if (is != null) {
+                        try {
+                            is.close();
+                        } catch (IOException e) {
+                        }
+                    }
+                }
+            } catch (IOException e) {
+                Log.e(LOG_TAG, "IOException was emitted: " + e.getMessage());
+                return false;
+            } catch (VCardNestedException e) {
+                // In the first scan, we may (correctly) encounter this exception.
+                // We assume that we were able to detect the type of vCard before
+                // the exception being thrown.
+                //
+                // In the second scan, we may (inappropriately) encounter it.
+                // We silently ignore it, since
+                // - It is really unusual situation.
+                // - We cannot handle it by definition.
+                // - Users cannot either.
+                // - We should not accept unnecessarily complicated vCard, possibly by wrong manner.
+                Log.w(LOG_TAG, "Nested Exception is found (it may be false-positive).");
+            } catch (VCardNotSupportedException e) {
+                return false;
+            } catch (VCardException e) {
+                return false;
+            }
+            return true;
+        }
+
+        private void doErrorNotification(int id) {
+            final Notification notification = new Notification();
+            notification.icon = android.R.drawable.stat_sys_download_done;
+            final String title = mContext.getString(R.string.reading_vcard_failed_title);
+            final PendingIntent intent =
+                    PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+            notification.setLatestEventInfo(mContext, title, "", intent);
+            mNotificationManager.notify(id, notification);
+        }
+
+        private void doFinishNotification(int id, Uri[] uris) {
+            final Notification notification = new Notification();
+            notification.icon = android.R.drawable.stat_sys_download_done;
+            final String title = mContext.getString(R.string.reading_vcard_finished_title);
+
+            final Intent intent;
+            final long rawContactId = ContentUris.parseId(mCreatedUris.get(0));
+            final Uri contactUri = RawContacts.getContactLookupUri(
+                    getContentResolver(), ContentUris.withAppendedId(
+                            RawContacts.CONTENT_URI, rawContactId));
+            intent = new Intent(Intent.ACTION_VIEW, contactUri);
+
+            final String text = ((uris.length == 1) ? uris[0].getPath() : "");
+            final PendingIntent pendingIntent =
+                    PendingIntent.getActivity(mContext, 0, intent, 0);
+            notification.setLatestEventInfo(mContext, title, text, pendingIntent);
+            mNotificationManager.notify(id, notification);
+        }
+
+        // We need synchronized since we need to handle mCanceled and mVCardParser at once.
+        public synchronized void cancel() {
+            mCanceled = true;
+            if (mVCardParser != null) {
+                mVCardParser.cancel();
+            }
+        }
+
+        public void onCancel(DialogInterface dialog) {
+            cancel();
+        }
+    }
+
+    private static class PendingInput {
+        public final Account account;
+        public final Uri[] uris;
+        public final int id;
+
+        public PendingInput(Account account, Uri[] uris, int id) {
+            this.account = account;
+            this.uris = uris;
+            this.id = id;
+        }
+    }
+
+    // The two classes bellow must be called inside the synchronized block, using this context.
+    private boolean mNowRunning;
+    private final Queue<PendingInput> mPendingInputs = new LinkedList<PendingInput>();
+
+    private NotificationManager mNotificationManager;
+    private Thread mThread;
+    private int mTotalCount;
+    private int mCurrentCount;
+
+    private Uri[] tryGetUris(Intent intent) {
+        final String[] uriStrings =
+                intent.getStringArrayExtra(ImportVCardActivity.VCARD_URI_ARRAY);
+        if (uriStrings == null || uriStrings.length == 0) {
+            Log.e(LOG_TAG, "Given uri array is empty");
+            return null;
+        }
+
+        final int length = uriStrings.length;
+        final Uri[] uris = new Uri[length];
+        for (int i = 0; i < length; i++) {
+            uris[i] = Uri.parse(uriStrings[i]);
+        }
+
+        return uris;
+    }
+
+    private Account tryGetAccount(Intent intent) {
+        if (intent == null) {
+            Log.w(LOG_TAG, "Intent is null");
+            return null;
+        }
+
+        final String accountName = intent.getStringExtra("account_name");
+        final String accountType = intent.getStringExtra("account_type");
+        if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
+            return new Account(accountName, accountType);
+        } else {
+            Log.w(LOG_TAG, "Account is not set.");
+            return null;
+        }
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        if (mNotificationManager == null) {
+            mNotificationManager =
+                    (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
+        }
+
+        final Account account = tryGetAccount(intent);
+        final Uri[] uris = tryGetUris(intent);
+        if (uris == null) {
+            Log.e(LOG_TAG, "Uris are null.");
+            Toast.makeText(this, getString(R.string.reading_vcard_failed_title),
+                    Toast.LENGTH_LONG).show();
+            stopSelf();
+            return START_NOT_STICKY;
+        }
+
+        synchronized (this) {
+            mPendingInputs.add(new PendingInput(account, uris, startId));
+            if (!mNowRunning) {
+                Toast.makeText(this, getString(R.string.vcard_importer_start_message),
+                        Toast.LENGTH_LONG).show();
+                // Assume thread is alredy broken.
+                // Even when it still exists, it never scan the PendingInput newly added above.
+                mNowRunning = true;
+                mThread = new VCardReadThread();
+                mThread.start();
+            } else {
+                Toast.makeText(this, getString(R.string.vcard_importer_will_start_message),
+                        Toast.LENGTH_LONG).show();
+            }
+        }
+
+        return START_NOT_STICKY;
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
+    }
+}
diff --git a/src/com/android/contacts/JoinContactActivity.java b/src/com/android/contacts/JoinContactActivity.java
new file mode 100644
index 0000000..501739d
--- /dev/null
+++ b/src/com/android/contacts/JoinContactActivity.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts;
+
+
+import com.android.contacts.list.JoinContactListFragment;
+import com.android.contacts.list.OnContactPickerActionListener;
+
+import android.app.Activity;
+import android.app.FragmentTransaction;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+/**
+ * An activity that shows a list of contacts that can be joined with the target contact.
+ */
+public class JoinContactActivity extends Activity {
+
+    private static final String TAG = "JoinContactActivity";
+
+    /**
+     * The action for the join contact activity.
+     * <p>
+     * Input: extra field {@link #EXTRA_TARGET_CONTACT_ID} is the aggregate ID.
+     * TODO: move to {@link ContactsContract}.
+     */
+    public static final String JOIN_CONTACT = "com.android.contacts.action.JOIN_CONTACT";
+
+    /**
+     * Used with {@link #JOIN_CONTACT} to give it the target for aggregation.
+     * <p>
+     * Type: LONG
+     */
+    public static final String EXTRA_TARGET_CONTACT_ID = "com.android.contacts.action.CONTACT_ID";
+
+    private long mTargetContactId;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        Intent intent = getIntent();
+        mTargetContactId = intent.getLongExtra(EXTRA_TARGET_CONTACT_ID, -1);
+        if (mTargetContactId == -1) {
+            Log.e(TAG, "Intent " + intent.getAction() + " is missing required extra: "
+                    + EXTRA_TARGET_CONTACT_ID);
+            setResult(RESULT_CANCELED);
+            finish();
+            return;
+        }
+
+        JoinContactListFragment fragment = new JoinContactListFragment();
+        fragment.setTargetContactId(mTargetContactId);
+        fragment.setOnContactPickerActionListener(new OnContactPickerActionListener() {
+            public void onPickContactAction(Uri contactUri) {
+                Intent intent = new Intent(null, contactUri);
+                setResult(RESULT_OK, intent);
+                finish();
+            }
+
+            public void onSearchAllContactsAction(String string) {
+            }
+
+            public void onShortcutIntentCreated(Intent intent) {
+            }
+
+            public void onCreateNewContactAction() {
+            }
+        });
+
+        FragmentTransaction transaction = openFragmentTransaction();
+        transaction.add(fragment, android.R.id.content);
+        transaction.commit();
+    }
+}
diff --git a/src/com/android/contacts/MultiplePhonePickerActivity.java b/src/com/android/contacts/MultiplePhonePickerActivity.java
new file mode 100644
index 0000000..8a56d20
--- /dev/null
+++ b/src/com/android/contacts/MultiplePhonePickerActivity.java
@@ -0,0 +1,226 @@
+/*
+ * 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.list.MultiplePhonePickerFragment;
+import com.android.contacts.list.OnMultiplePhoneNumberPickerActionListener;
+
+import android.app.Activity;
+import android.app.FragmentTransaction;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.provider.ContactsContract.Intents;
+import android.view.Menu;
+import android.view.MenuInflater;
+
+/**
+ * Displays of phone numbers and allows selection of multiple numbers.
+ */
+public class MultiplePhonePickerActivity extends Activity {
+
+    /**
+     * Display only selected recipients or not in MODE_PICK_MULTIPLE_PHONES mode
+     */
+    private boolean mShowSelectedOnly = false;
+
+    private MultiplePhonePickerFragment mListFragment;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mListFragment = new MultiplePhonePickerFragment();
+        mListFragment.setOnMultiplePhoneNumberPickerActionListener(
+                new OnMultiplePhoneNumberPickerActionListener() {
+
+            public void onPhoneNumbersSelectedAction(Uri[] dataUris) {
+                returnActivityResult(dataUris);
+            }
+
+            public void onFinishAction() {
+                finish();
+            }
+        });
+
+        Parcelable[] extras = getIntent().getParcelableArrayExtra(Intents.EXTRA_PHONE_URIS);
+        mListFragment.setSelectedUris(extras);
+        FragmentTransaction transaction = openFragmentTransaction();
+        transaction.add(mListFragment, android.R.id.content);
+        transaction.commit();
+    }
+
+    @Override
+    public void onBackPressed() {
+        returnActivityResult(mListFragment.getSelectedUris());
+        super.onBackPressed();
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle icicle) {
+        super.onSaveInstanceState(icicle);
+        mListFragment.onSaveInstanceState(icicle);
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        super.onCreateOptionsMenu(menu);
+
+        final MenuInflater inflater = getMenuInflater();
+        inflater.inflate(R.menu.pick, menu);
+        return true;
+    }
+
+    /*
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        if (mShowSelectedOnly) {
+            menu.findItem(R.id.menu_display_selected).setVisible(false);
+            menu.findItem(R.id.menu_display_all).setVisible(true);
+            menu.findItem(R.id.menu_select_all).setVisible(false);
+            menu.findItem(R.id.menu_select_none).setVisible(false);
+            return true;
+        }
+        menu.findItem(R.id.menu_display_all).setVisible(false);
+        menu.findItem(R.id.menu_display_selected).setVisible(true);
+        if (mUserSelection.isAllSelected()) {
+            menu.findItem(R.id.menu_select_all).setVisible(false);
+            menu.findItem(R.id.menu_select_none).setVisible(true);
+        } else {
+            menu.findItem(R.id.menu_select_all).setVisible(true);
+            menu.findItem(R.id.menu_select_none).setVisible(false);
+        }
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.menu_select_all: {
+                mUserSelection.setAllPhonesSelected(true);
+                checkAll(true);
+                updateWidgets(true);
+                return true;
+            }
+            case R.id.menu_select_none: {
+                mUserSelection.setAllPhonesSelected(false);
+                checkAll(false);
+                updateWidgets(true);
+                return true;
+            }
+            case R.id.menu_display_selected: {
+                mShowSelectedOnly = true;
+                startQuery();
+                return true;
+            }
+            case R.id.menu_display_all: {
+                mShowSelectedOnly = false;
+                startQuery();
+                return true;
+            }
+        }
+        return super.onOptionsItemSelected(item);
+    }
+*/
+    @Override
+    public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData,
+            boolean globalSearch) {
+        // TODO
+//        if (mProviderStatus != ProviderStatus.STATUS_NORMAL) {
+//            return;
+//        }
+//
+//        if (globalSearch) {
+//            super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
+//        } else {
+//            if (!mSearchMode && (mMode & MODE_MASK_NO_FILTER) == 0) {
+//                if ((mMode & MODE_MASK_PICKER) != 0) {
+//                    Bundle extras = getIntent().getExtras();
+//                    if (extras == null) {
+//                        extras = new Bundle();
+//                    }
+//                    mUserSelection.fillSelectionForSearchMode(extras);
+//                    ContactsSearchManager.startSearchForResult(this, initialQuery,
+//                            SUBACTIVITY_FILTER, extras);
+//                } else {
+//                    ContactsSearchManager.startSearch(this, initialQuery);
+//                }
+//            }
+//        }
+    }
+
+//    @Override
+//    protected void startQuery(Uri uri, String[] projection) {
+//        // Filter unknown phone numbers first.
+//        mPhoneNumberAdapter.doFilter(null, mShowSelectedOnly);
+//        if (mShowSelectedOnly) {
+//            StringBuilder idSetBuilder = new StringBuilder();
+//            Iterator<Long> itr = mUserSelection.getSelectedPhonIds();
+//            if (itr.hasNext()) {
+//                idSetBuilder.append(Long.toString(itr.next()));
+//            }
+//            while (itr.hasNext()) {
+//                idSetBuilder.append(',');
+//                idSetBuilder.append(Long.toString(itr.next()));
+//            }
+//            String whereClause = Phone._ID + " IN (" + idSetBuilder.toString() + ")";
+//            mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, whereClause, null,
+//                    getSortOrder(projection));
+//        } else {
+//            mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
+//                    projection, CLAUSE_ONLY_VISIBLE, null, getSortOrder(projection));
+//        }
+//    }
+
+//    @Override
+//    public Cursor doFilter(String filter) {
+//        String[] projection = getProjectionForQuery();
+//        if (mSearchMode && TextUtils.isEmpty(mListFragment.getQueryString())) {
+//            return new MatrixCursor(projection);
+//        }
+//
+//        final ContentResolver resolver = getContentResolver();
+//        // Filter phone numbers as well.
+//        mPhoneNumberAdapter.doFilter(filter, mShowSelectedOnly);
+//
+//        Uri uri = getUriToQuery();
+//        if (!TextUtils.isEmpty(filter)) {
+//            uri = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(filter));
+//        }
+//        return resolver.query(uri, projection, CLAUSE_ONLY_VISIBLE, null, getSortOrder(projection));
+//    }
+
+    public void returnActivityResult(Uri[] dataUris) {
+        Intent intent = new Intent();
+        intent.putExtra(Intents.EXTRA_PHONE_URIS, dataUris);
+        setResult(RESULT_OK, intent);
+        finish();
+    }
+
+    private void checkAll(boolean checked) {
+        // TODO fix this. It should iterate over the cursor rather than the views in the list.
+        /*
+        final ListView listView = getListView();
+        int childCount = listView.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final ContactListItemView child = (ContactListItemView)listView.getChildAt(i);
+            child.getCheckBoxView().setChecked(checked);
+        }
+        */
+    }
+}
diff --git a/src/com/android/contacts/PhoneDisambigDialog.java b/src/com/android/contacts/PhoneDisambigDialog.java
index d8cb14e..3b32b57 100644
--- a/src/com/android/contacts/PhoneDisambigDialog.java
+++ b/src/com/android/contacts/PhoneDisambigDialog.java
@@ -49,13 +49,12 @@
  * one will be chosen to make a call or initiate an sms message.
  */
 public class PhoneDisambigDialog implements DialogInterface.OnClickListener,
-        DialogInterface.OnDismissListener, CompoundButton.OnCheckedChangeListener{
+        CompoundButton.OnCheckedChangeListener{
 
     private boolean mMakePrimary = false;
     private Context mContext;
     private AlertDialog mDialog;
     private boolean mSendSms;
-    private Cursor mPhonesCursor;
     private ListAdapter mPhonesAdapter;
     private ArrayList<PhoneItem> mPhoneItemList;
 
@@ -66,9 +65,9 @@
     public PhoneDisambigDialog(Context context, Cursor phonesCursor, boolean sendSms) {
         mContext = context;
         mSendSms = sendSms;
-        mPhonesCursor = phonesCursor;
-
         mPhoneItemList = makePhoneItemsList(phonesCursor);
+        phonesCursor.close();
+
         Collapser.collapseList(mPhoneItemList);
 
         mPhonesAdapter = new PhonesAdapter(mContext, mPhoneItemList, mSendSms);
@@ -90,6 +89,10 @@
         mDialog = dialogBuilder.create();
     }
 
+    public void setOnDismissListener(DialogInterface.OnDismissListener dismissListener) {
+        mDialog.setOnDismissListener(dismissListener);
+    }
+
     /**
      * Show the dialog.
      */
@@ -129,10 +132,6 @@
         mMakePrimary = isChecked;
     }
 
-    public void onDismiss(DialogInterface dialog) {
-        mPhonesCursor.close();
-    }
-
     private static class PhonesAdapter extends ArrayAdapter<PhoneItem> {
         private final boolean sendSms;
         private final Sources mSources;
diff --git a/src/com/android/contacts/ProgressShower.java b/src/com/android/contacts/ProgressShower.java
deleted file mode 100644
index a5ad2a2..0000000
--- a/src/com/android/contacts/ProgressShower.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.contacts;
-
-import android.app.ProgressDialog;
-import android.content.Context;
-import android.os.Handler;
-import android.pim.vcard.VCardEntry;
-import android.pim.vcard.VCardEntryHandler;
-import android.pim.vcard.VCardConfig;
-import android.util.Log;
-
-public class ProgressShower implements VCardEntryHandler {
-    public static final String LOG_TAG = "vcard.ProgressShower"; 
-
-    private final Context mContext;
-    private final Handler mHandler;
-    private final ProgressDialog mProgressDialog;
-    private final String mProgressMessage;
-
-    private long mTime;
-    
-    private class ShowProgressRunnable implements Runnable {
-        private VCardEntry mContact;
-        
-        public ShowProgressRunnable(VCardEntry contact) {
-            mContact = contact;
-        }
-        
-        public void run() {
-            mProgressDialog.setMessage( mProgressMessage + "\n" + 
-                    mContact.getDisplayName());
-            mProgressDialog.incrementProgressBy(1);
-        }
-    }
-    
-    public ProgressShower(ProgressDialog progressDialog,
-            String progressMessage,
-            Context context,
-            Handler handler) {
-        mContext = context;
-        mHandler = handler;
-        mProgressDialog = progressDialog;
-        mProgressMessage = progressMessage;
-    }
-
-    public void onStart() {
-    }
-
-    public void onEntryCreated(VCardEntry contactStruct) {
-        long start = System.currentTimeMillis();
-        
-        if (!contactStruct.isIgnorable()) {
-            if (mProgressDialog != null && mProgressMessage != null) {
-                if (mHandler != null) {
-                    mHandler.post(new ShowProgressRunnable(contactStruct));
-                } else {
-                    mProgressDialog.setMessage(mContext.getString(R.string.progress_shower_message,
-                            mProgressMessage, 
-                            contactStruct.getDisplayName()));
-                }
-            }
-        }
-        
-        mTime += System.currentTimeMillis() - start;
-    }
-
-    public void onEnd() {
-        if (VCardConfig.showPerformanceLog()) {
-            Log.d(LOG_TAG,
-                    String.format("Time to progress a dialog: %d ms", mTime));
-        }
-    }
-}
diff --git a/src/com/android/contacts/SearchEditText.java b/src/com/android/contacts/SearchEditText.java
deleted file mode 100644
index 7683f23..0000000
--- a/src/com/android/contacts/SearchEditText.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * 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 android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.view.KeyEvent;
-import android.widget.EditText;
-
-/**
- * A custom text editor that helps automatically dismiss the activity along with the soft
- * keyboard.
- */
-public class SearchEditText extends EditText {
-
-    private boolean mMagnifyingGlassShown = true;
-    private Drawable mMagnifyingGlass;
-
-    public SearchEditText(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        mMagnifyingGlass = getCompoundDrawables()[2];
-    }
-
-    /**
-     * Conditionally shows a magnifying glass icon on the right side of the text field
-     * when the text it empty.
-     */
-    @Override
-    public boolean onPreDraw() {
-        boolean emptyText = TextUtils.isEmpty(getText());
-        if (mMagnifyingGlassShown != emptyText) {
-            mMagnifyingGlassShown = emptyText;
-            if (mMagnifyingGlassShown) {
-                setCompoundDrawables(null, null, mMagnifyingGlass, null);
-            } else {
-                setCompoundDrawables(null, null, null, null);
-            }
-            return false;
-        }
-        return super.onPreDraw();
-    }
-
-    /**
-     * Forwards the onKeyPreIme call to the view's activity.
-     */
-    @Override
-    public boolean onKeyPreIme(int keyCode, KeyEvent event) {
-        if (((ContactsListActivity)getContext()).onKeyPreIme(keyCode, event)) {
-            return true;
-        }
-        return super.onKeyPreIme(keyCode, event);
-    }
-}
diff --git a/src/com/android/contacts/SelectAccountActivity.java b/src/com/android/contacts/SelectAccountActivity.java
new file mode 100644
index 0000000..cef00e0
--- /dev/null
+++ b/src/com/android/contacts/SelectAccountActivity.java
@@ -0,0 +1,106 @@
+/*
+ * 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 android.accounts.Account;
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.contacts.model.Sources;
+import com.android.contacts.util.AccountSelectionUtil;
+
+import java.util.List;
+
+public class SelectAccountActivity extends Activity {
+    private static final String LOG_TAG = "SelectAccountActivity";
+
+    /* package */ static final String ACCOUNT_NAME = "account_name";
+    /* package */ static final String ACCOUNT_TYPE = "account_type";
+
+    private class CancelListener
+            implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
+        public void onClick(DialogInterface dialog, int which) {
+            finish();
+        }
+        public void onCancel(DialogInterface dialog) {
+            finish();
+        }
+    }
+
+    private AccountSelectionUtil.AccountSelectedListener mAccountSelectionListener;
+
+    @Override
+    protected void onCreate(Bundle bundle) {
+        super.onCreate(bundle);
+
+        // There's three possibilities:
+        // - more than one accounts -> ask the user
+        // - just one account -> use the account without asking the user
+        // - no account -> use phone-local storage without asking the user
+        final int resId = R.string.import_from_sdcard;
+        final Sources sources = Sources.getInstance(this);
+        final List<Account> accountList = sources.getAccounts(true);
+        if (accountList.size() == 0) {
+            Log.w(LOG_TAG, "Account does not exist");
+            finish();
+        } else if (accountList.size() == 1) {
+            final Account account = accountList.get(0);
+            final Intent intent = new Intent();
+            intent.putExtra(ACCOUNT_NAME, account.name);
+            intent.putExtra(ACCOUNT_TYPE, account.type);
+            setResult(RESULT_OK, intent);
+            finish();
+        }
+
+        // Multiple accounts. Let users to select one.
+        mAccountSelectionListener =
+                new AccountSelectionUtil.AccountSelectedListener(
+                        this, accountList, resId) {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        dialog.dismiss();
+                        final Account account = mAccountList.get(which);
+                        final Intent intent = new Intent();
+                        intent.putExtra(ACCOUNT_NAME, account.name);
+                        intent.putExtra(ACCOUNT_TYPE, account.type);
+                        setResult(RESULT_OK, intent);
+                        finish();
+                    }
+                };
+        showDialog(resId);
+        return;
+    }
+
+    @Override
+    protected Dialog onCreateDialog(int resId, Bundle bundle) {
+        switch (resId) {
+            case R.string.import_from_sdcard: {
+                if (mAccountSelectionListener == null) {
+                    throw new NullPointerException(
+                            "mAccountSelectionListener must not be null.");
+                }
+                return AccountSelectionUtil.getSelectAccountDialog(this, resId,
+                        mAccountSelectionListener,
+                        new CancelListener());
+            }
+        }
+        return super.onCreateDialog(resId, bundle);
+    }
+}
diff --git a/src/com/android/contacts/ViewContactActivity.java b/src/com/android/contacts/ViewContactActivity.java
deleted file mode 100644
index ead6a4a..0000000
--- a/src/com/android/contacts/ViewContactActivity.java
+++ /dev/null
@@ -1,1363 +0,0 @@
-/*
- * Copyright (C) 2007 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.Collapser.Collapsible;
-import com.android.contacts.model.ContactsSource;
-import com.android.contacts.model.Sources;
-import com.android.contacts.model.ContactsSource.DataKind;
-import com.android.contacts.ui.EditContactActivity;
-import com.android.contacts.util.Constants;
-import com.android.contacts.util.DataStatus;
-import com.android.contacts.util.NotifyingAsyncQueryHandler;
-import com.android.internal.telephony.ITelephony;
-import com.android.internal.widget.ContactHeaderWidget;
-import com.google.android.collect.Lists;
-import com.google.android.collect.Maps;
-
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.content.ActivityNotFoundException;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Entity;
-import android.content.EntityIterator;
-import android.content.Intent;
-import android.content.Entity.NamedContentValues;
-import android.content.res.Resources;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.graphics.drawable.Drawable;
-import android.net.ParseException;
-import android.net.Uri;
-import android.net.WebAddress;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.RemoteException;
-import android.os.ServiceManager;
-import android.provider.ContactsContract;
-import android.provider.ContactsContract.AggregationExceptions;
-import android.provider.ContactsContract.CommonDataKinds;
-import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.DisplayNameSources;
-import android.provider.ContactsContract.RawContacts;
-import android.provider.ContactsContract.RawContactsEntity;
-import android.provider.ContactsContract.StatusUpdates;
-import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.Im;
-import android.provider.ContactsContract.CommonDataKinds.Nickname;
-import android.provider.ContactsContract.CommonDataKinds.Note;
-import android.provider.ContactsContract.CommonDataKinds.Organization;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
-import android.provider.ContactsContract.CommonDataKinds.Website;
-import android.telephony.PhoneNumberUtils;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.ContextMenu;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.Window;
-import android.view.ContextMenu.ContextMenuInfo;
-import android.widget.AdapterView;
-import android.widget.ImageView;
-import android.widget.ListView;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-
-/**
- * Displays the details of a specific contact.
- */
-public class ViewContactActivity extends Activity
-        implements View.OnCreateContextMenuListener, DialogInterface.OnClickListener,
-        AdapterView.OnItemClickListener, NotifyingAsyncQueryHandler.AsyncQueryListener {
-    private static final String TAG = "ViewContact";
-
-    private static final boolean SHOW_SEPARATORS = false;
-
-    private static final int DIALOG_CONFIRM_DELETE = 1;
-    private static final int DIALOG_CONFIRM_READONLY_DELETE = 2;
-    private static final int DIALOG_CONFIRM_MULTIPLE_DELETE = 3;
-    private static final int DIALOG_CONFIRM_READONLY_HIDE = 4;
-
-    private static final int REQUEST_JOIN_CONTACT = 1;
-    private static final int REQUEST_EDIT_CONTACT = 2;
-
-    public static final int MENU_ITEM_MAKE_DEFAULT = 3;
-
-    protected Uri mLookupUri;
-    private ContentResolver mResolver;
-    private ViewAdapter mAdapter;
-    private int mNumPhoneNumbers = 0;
-
-    /**
-     * A list of distinct contact IDs included in the current contact.
-     */
-    private ArrayList<Long> mRawContactIds = new ArrayList<Long>();
-
-    /* package */ ArrayList<ViewEntry> mPhoneEntries = new ArrayList<ViewEntry>();
-    /* package */ ArrayList<ViewEntry> mSmsEntries = new ArrayList<ViewEntry>();
-    /* package */ ArrayList<ViewEntry> mEmailEntries = new ArrayList<ViewEntry>();
-    /* package */ ArrayList<ViewEntry> mPostalEntries = new ArrayList<ViewEntry>();
-    /* package */ ArrayList<ViewEntry> mImEntries = new ArrayList<ViewEntry>();
-    /* package */ ArrayList<ViewEntry> mNicknameEntries = new ArrayList<ViewEntry>();
-    /* package */ ArrayList<ViewEntry> mOrganizationEntries = new ArrayList<ViewEntry>();
-    /* package */ ArrayList<ViewEntry> mGroupEntries = new ArrayList<ViewEntry>();
-    /* package */ ArrayList<ViewEntry> mOtherEntries = new ArrayList<ViewEntry>();
-    /* package */ ArrayList<ArrayList<ViewEntry>> mSections = new ArrayList<ArrayList<ViewEntry>>();
-
-    private Cursor mCursor;
-
-    protected ContactHeaderWidget mContactHeaderWidget;
-    private NotifyingAsyncQueryHandler mHandler;
-
-    protected LayoutInflater mInflater;
-
-    protected int mReadOnlySourcesCnt;
-    protected int mWritableSourcesCnt;
-    protected boolean mAllRestricted;
-
-    protected Uri mPrimaryPhoneUri = null;
-
-    protected ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>();
-
-    private static final int TOKEN_ENTITIES = 0;
-    private static final int TOKEN_STATUSES = 1;
-
-    private boolean mHasEntities = false;
-    private boolean mHasStatuses = false;
-
-    private long mNameRawContactId = -1;
-    private int mDisplayNameSource = DisplayNameSources.UNDEFINED;
-
-    private ArrayList<Entity> mEntities = Lists.newArrayList();
-    private HashMap<Long, DataStatus> mStatuses = Maps.newHashMap();
-
-    /**
-     * The view shown if the detail list is empty.
-     * We set this to the list view when first bind the adapter, so that it won't be shown while
-     * we're loading data.
-     */
-    private View mEmptyView;
-
-    private ContentObserver mObserver = new ContentObserver(new Handler()) {
-        @Override
-        public boolean deliverSelfNotifications() {
-            return true;
-        }
-
-        @Override
-        public void onChange(boolean selfChange) {
-            if (mCursor != null && !mCursor.isClosed()) {
-                startEntityQuery();
-            }
-        }
-    };
-
-    public void onClick(DialogInterface dialog, int which) {
-        closeCursor();
-        getContentResolver().delete(mLookupUri, null, null);
-        finish();
-    }
-
-    private ListView mListView;
-    private boolean mShowSmsLinksForAllPhones;
-
-    @Override
-    protected void onCreate(Bundle icicle) {
-        super.onCreate(icicle);
-
-        final Intent intent = getIntent();
-        Uri data = intent.getData();
-        String authority = data.getAuthority();
-        if (ContactsContract.AUTHORITY.equals(authority)) {
-            mLookupUri = data;
-        } else if (android.provider.Contacts.AUTHORITY.equals(authority)) {
-            final long rawContactId = ContentUris.parseId(data);
-            mLookupUri = RawContacts.getContactLookupUri(getContentResolver(),
-                    ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
-
-        }
-        mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-
-        requestWindowFeature(Window.FEATURE_NO_TITLE);
-        setContentView(R.layout.contact_card_layout);
-
-        mContactHeaderWidget = (ContactHeaderWidget) findViewById(R.id.contact_header_widget);
-        mContactHeaderWidget.showStar(true);
-        mContactHeaderWidget.setExcludeMimes(new String[] {
-            Contacts.CONTENT_ITEM_TYPE
-        });
-
-        mHandler = new NotifyingAsyncQueryHandler(this, this);
-
-        mListView = (ListView) findViewById(R.id.contact_data);
-        mListView.setOnCreateContextMenuListener(this);
-        mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
-        mListView.setOnItemClickListener(this);
-        // Don't set it to mListView yet.  We do so later when we bind the adapter.
-        mEmptyView = findViewById(android.R.id.empty);
-
-        mResolver = getContentResolver();
-
-        // Build the list of sections. The order they're added to mSections dictates the
-        // order they are displayed in the list.
-        mSections.add(mPhoneEntries);
-        mSections.add(mSmsEntries);
-        mSections.add(mEmailEntries);
-        mSections.add(mImEntries);
-        mSections.add(mPostalEntries);
-        mSections.add(mNicknameEntries);
-        mSections.add(mOrganizationEntries);
-        mSections.add(mGroupEntries);
-        mSections.add(mOtherEntries);
-
-        //TODO Read this value from a preference
-        mShowSmsLinksForAllPhones = true;
-    }
-
-    @Override
-    protected void onResume() {
-        super.onResume();
-        startEntityQuery();
-    }
-
-    @Override
-    protected void onPause() {
-        super.onPause();
-        closeCursor();
-    }
-
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-        closeCursor();
-    }
-
-    @Override
-    protected Dialog onCreateDialog(int id) {
-        switch (id) {
-            case DIALOG_CONFIRM_DELETE:
-                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, this)
-                        .setCancelable(false)
-                        .create();
-            case DIALOG_CONFIRM_READONLY_DELETE:
-                return new AlertDialog.Builder(this)
-                        .setTitle(R.string.deleteConfirmation_title)
-                        .setIcon(android.R.drawable.ic_dialog_alert)
-                        .setMessage(R.string.readOnlyContactDeleteConfirmation)
-                        .setNegativeButton(android.R.string.cancel, null)
-                        .setPositiveButton(android.R.string.ok, this)
-                        .setCancelable(false)
-                        .create();
-            case DIALOG_CONFIRM_MULTIPLE_DELETE:
-                return new AlertDialog.Builder(this)
-                        .setTitle(R.string.deleteConfirmation_title)
-                        .setIcon(android.R.drawable.ic_dialog_alert)
-                        .setMessage(R.string.multipleContactDeleteConfirmation)
-                        .setNegativeButton(android.R.string.cancel, null)
-                        .setPositiveButton(android.R.string.ok, this)
-                        .setCancelable(false)
-                        .create();
-            case DIALOG_CONFIRM_READONLY_HIDE: {
-                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, this)
-                        .create();
-            }
-
-        }
-        return null;
-    }
-
-    /** {@inheritDoc} */
-    public void onQueryComplete(int token, Object cookie, final Cursor cursor) {
-        if (token == TOKEN_STATUSES) {
-            try {
-                // Read available social rows and consider binding
-                readStatuses(cursor);
-            } finally {
-                if (cursor != null) {
-                    cursor.close();
-                }
-            }
-            considerBindData();
-            return;
-        }
-
-        // One would think we could just iterate over the Cursor
-        // directly here, as the result set should be small, and we've
-        // already run the query in an AsyncTask, but a lot of ANRs
-        // were being reported in this code nonetheless.  See bug
-        // 2539603 for details.  The real bug which makes this result
-        // set huge and CPU-heavy may be elsewhere.
-        // TODO: if we keep this async, perhaps the entity iteration
-        // should also be original AsyncTask, rather than ping-ponging
-        // between threads like this.
-        final ArrayList<Entity> oldEntities = mEntities;
-        (new AsyncTask<Void, Void, ArrayList<Entity>>() {
-            @Override
-            protected ArrayList<Entity> doInBackground(Void... params) {
-                ArrayList<Entity> newEntities = new ArrayList<Entity>(cursor.getCount());
-                EntityIterator iterator = RawContacts.newEntityIterator(cursor);
-                try {
-                    while (iterator.hasNext()) {
-                        Entity entity = iterator.next();
-                        newEntities.add(entity);
-                    }
-                } finally {
-                    iterator.close();
-                }
-                return newEntities;
-            }
-
-            @Override
-            protected void onPostExecute(ArrayList<Entity> newEntities) {
-                if (newEntities == null) {
-                    // There was an error loading.
-                    return;
-                }
-                synchronized (ViewContactActivity.this) {
-                    if (mEntities != oldEntities) {
-                        // Multiple async tasks were in flight and we
-                        // lost the race.
-                        return;
-                    }
-                    mEntities = newEntities;
-                    mHasEntities = true;
-                }
-                considerBindData();
-            }
-        }).execute();
-    }
-
-    private long getRefreshedContactId() {
-        Uri freshContactUri = Contacts.lookupContact(getContentResolver(), mLookupUri);
-        if (freshContactUri != null) {
-            return ContentUris.parseId(freshContactUri);
-        }
-        return -1;
-    }
-
-    /**
-     * Read from the given {@link Cursor} and build a set of {@link DataStatus}
-     * objects to match any valid statuses found.
-     */
-    private synchronized void readStatuses(Cursor cursor) {
-        mStatuses.clear();
-
-        // Walk found statuses, creating internal row for each
-        while (cursor.moveToNext()) {
-            final DataStatus status = new DataStatus(cursor);
-            final long dataId = cursor.getLong(StatusQuery._ID);
-            mStatuses.put(dataId, status);
-        }
-
-        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();
-
-        // 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);
-            mCursor = setupContactCursor(mResolver, mLookupUri);
-        }
-
-        // 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();
-            Log.e(TAG, "invalid contact uri: " + mLookupUri);
-            finish();
-            return;
-        }
-
-        final long contactId = ContentUris.parseId(mLookupUri);
-
-        mNameRawContactId =
-                mCursor.getLong(mCursor.getColumnIndex(Contacts.NAME_RAW_CONTACT_ID));
-        mDisplayNameSource =
-                mCursor.getInt(mCursor.getColumnIndex(Contacts.DISPLAY_NAME_SOURCE));
-
-        mCursor.registerContentObserver(mObserver);
-
-        // 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 + "=?", 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);
-
-        mContactHeaderWidget.bindFromContactLookupUri(mLookupUri);
-    }
-
-    private void closeCursor() {
-        if (mCursor != null) {
-            mCursor.unregisterContentObserver(mObserver);
-            mCursor.close();
-            mCursor = null;
-        }
-    }
-
-    /**
-     * Consider binding views after any of several background queries has
-     * completed. We check internal flags and only bind when all data has
-     * arrived.
-     */
-    private void considerBindData() {
-        if (mHasEntities && mHasStatuses) {
-            bindData();
-        }
-    }
-
-    private void bindData() {
-
-        // Build up the contact entries
-        buildEntries();
-
-        // Collapse similar data items in select sections.
-        Collapser.collapseList(mPhoneEntries);
-        Collapser.collapseList(mSmsEntries);
-        Collapser.collapseList(mEmailEntries);
-        Collapser.collapseList(mPostalEntries);
-        Collapser.collapseList(mImEntries);
-
-        if (mAdapter == null) {
-            mAdapter = new ViewAdapter(this, mSections);
-            mListView.setAdapter(mAdapter);
-        } else {
-            mAdapter.setSections(mSections, SHOW_SEPARATORS);
-        }
-        mListView.setEmptyView(mEmptyView);
-    }
-
-    @Override
-    public boolean onCreateOptionsMenu(Menu menu) {
-        super.onCreateOptionsMenu(menu);
-
-        final MenuInflater inflater = getMenuInflater();
-        inflater.inflate(R.menu.view, menu);
-        return true;
-    }
-
-    @Override
-    public boolean onPrepareOptionsMenu(Menu menu) {
-        super.onPrepareOptionsMenu(menu);
-
-        // Only allow edit when we have at least one raw_contact id
-        final boolean hasRawContact = (mRawContactIds.size() > 0);
-        menu.findItem(R.id.menu_edit).setEnabled(hasRawContact);
-
-        // Only allow share when unrestricted contacts available
-        menu.findItem(R.id.menu_share).setEnabled(!mAllRestricted);
-
-        return true;
-    }
-
-    @Override
-    public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
-        AdapterView.AdapterContextMenuInfo info;
-        try {
-             info = (AdapterView.AdapterContextMenuInfo) menuInfo;
-        } catch (ClassCastException e) {
-            Log.e(TAG, "bad menuInfo", e);
-            return;
-        }
-
-        // This can be null sometimes, don't crash...
-        if (info == null) {
-            Log.e(TAG, "bad menuInfo");
-            return;
-        }
-
-        ViewEntry entry = ContactEntryAdapter.getEntry(mSections, info.position, SHOW_SEPARATORS);
-        menu.setHeaderTitle(R.string.contactOptionsTitle);
-        if (entry.mimetype.equals(CommonDataKinds.Phone.CONTENT_ITEM_TYPE)) {
-            menu.add(0, 0, 0, R.string.menu_call).setIntent(entry.intent);
-            menu.add(0, 0, 0, R.string.menu_sendSMS).setIntent(entry.secondaryIntent);
-            if (!entry.isPrimary) {
-                menu.add(0, MENU_ITEM_MAKE_DEFAULT, 0, R.string.menu_makeDefaultNumber);
-            }
-        } else if (entry.mimetype.equals(CommonDataKinds.Email.CONTENT_ITEM_TYPE)) {
-            menu.add(0, 0, 0, R.string.menu_sendEmail).setIntent(entry.intent);
-            if (!entry.isPrimary) {
-                menu.add(0, MENU_ITEM_MAKE_DEFAULT, 0, R.string.menu_makeDefaultEmail);
-            }
-        } else if (entry.mimetype.equals(CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE)) {
-            menu.add(0, 0, 0, R.string.menu_viewAddress).setIntent(entry.intent);
-        }
-    }
-
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        switch (item.getItemId()) {
-            case R.id.menu_edit: {
-                Long rawContactIdToEdit = null;
-                if (mRawContactIds.size() > 0) {
-                    rawContactIdToEdit = mRawContactIds.get(0);
-                } else {
-                    // There is no rawContact to edit.
-                    break;
-                }
-                Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
-                        rawContactIdToEdit);
-                startActivityForResult(new Intent(Intent.ACTION_EDIT, rawContactUri),
-                        REQUEST_EDIT_CONTACT);
-                break;
-            }
-            case R.id.menu_delete: {
-                // Get confirmation
-                if (mReadOnlySourcesCnt > 0 & mWritableSourcesCnt > 0) {
-                    showDialog(DIALOG_CONFIRM_READONLY_DELETE);
-                } else if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt == 0) {
-                    showDialog(DIALOG_CONFIRM_READONLY_HIDE);
-                } else if (mReadOnlySourcesCnt == 0 && mWritableSourcesCnt > 1) {
-                    showDialog(DIALOG_CONFIRM_MULTIPLE_DELETE);
-                } else {
-                    showDialog(DIALOG_CONFIRM_DELETE);
-                }
-                return true;
-            }
-            case R.id.menu_join: {
-                showJoinAggregateActivity();
-                return true;
-            }
-            case R.id.menu_options: {
-                showOptionsActivity();
-                return true;
-            }
-            case R.id.menu_share: {
-                if (mAllRestricted) return false;
-
-                // TODO: Keep around actual LOOKUP_KEY, or formalize method of extracting
-                final String lookupKey = Uri.encode(mLookupUri.getPathSegments().get(2));
-                final Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey);
-
-                final Intent intent = new Intent(Intent.ACTION_SEND);
-                intent.setType(Contacts.CONTENT_VCARD_TYPE);
-                intent.putExtra(Intent.EXTRA_STREAM, shareUri);
-
-                // Launch chooser to share contact via
-                final CharSequence chooseTitle = getText(R.string.share_via);
-                final Intent chooseIntent = Intent.createChooser(intent, chooseTitle);
-
-                try {
-                    startActivity(chooseIntent);
-                } catch (ActivityNotFoundException ex) {
-                    Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show();
-                }
-                return true;
-            }
-        }
-        return super.onOptionsItemSelected(item);
-    }
-
-    @Override
-    public boolean onContextItemSelected(MenuItem item) {
-        switch (item.getItemId()) {
-            case MENU_ITEM_MAKE_DEFAULT: {
-                if (makeItemDefault(item)) {
-                    return true;
-                }
-                break;
-            }
-        }
-
-        return super.onContextItemSelected(item);
-    }
-
-    private boolean makeItemDefault(MenuItem item) {
-        ViewEntry entry = getViewEntryForMenuItem(item);
-        if (entry == null) {
-            return false;
-        }
-
-        // Update the primary values in the data record.
-        ContentValues values = new ContentValues(1);
-        values.put(Data.IS_SUPER_PRIMARY, 1);
-        getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, entry.id),
-                values, null, null);
-        startEntityQuery();
-        return true;
-    }
-
-    /**
-     * Shows a list of aggregates that can be joined into the currently viewed aggregate.
-     */
-    public void showJoinAggregateActivity() {
-        long freshId = getRefreshedContactId();
-        if (freshId > 0) {
-            String displayName = null;
-            if (mCursor.moveToFirst()) {
-                displayName = mCursor.getString(0);
-            }
-            Intent intent = new Intent(ContactsListActivity.JOIN_AGGREGATE);
-            intent.putExtra(ContactsListActivity.EXTRA_AGGREGATE_ID, freshId);
-            if (displayName != null) {
-                intent.putExtra(ContactsListActivity.EXTRA_AGGREGATE_NAME, displayName);
-            }
-            startActivityForResult(intent, REQUEST_JOIN_CONTACT);
-        }
-    }
-
-    @Override
-    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
-        if (requestCode == REQUEST_JOIN_CONTACT) {
-            if (resultCode == RESULT_OK && intent != null) {
-                final long contactId = ContentUris.parseId(intent.getData());
-                joinAggregate(contactId);
-            }
-        } else if (requestCode == REQUEST_EDIT_CONTACT) {
-            if (resultCode == EditContactActivity.RESULT_CLOSE_VIEW_ACTIVITY) {
-                finish();
-            } else if (resultCode == Activity.RESULT_OK) {
-                mLookupUri = intent.getData();
-                if (mLookupUri == null) {
-                    finish();
-                }
-            }
-        }
-    }
-
-    private void joinAggregate(final long contactId) {
-        Cursor c = mResolver.query(RawContacts.CONTENT_URI, new String[] {RawContacts._ID},
-                RawContacts.CONTACT_ID + "=" + contactId, null, null);
-
-        try {
-            while(c.moveToNext()) {
-                long rawContactId = c.getLong(0);
-                setAggregationException(rawContactId, AggregationExceptions.TYPE_KEEP_TOGETHER);
-            }
-        } finally {
-            c.close();
-        }
-
-        Toast.makeText(this, R.string.contactsJoinedMessage, Toast.LENGTH_LONG).show();
-        startEntityQuery();
-    }
-
-    /**
-     * Given a contact ID sets an aggregation exception to either join the contact with the
-     * current aggregate or split off.
-     */
-    protected void setAggregationException(long rawContactId, int exceptionType) {
-        ContentValues values = new ContentValues(3);
-        for (long aRawContactId : mRawContactIds) {
-            if (aRawContactId != rawContactId) {
-                values.put(AggregationExceptions.RAW_CONTACT_ID1, aRawContactId);
-                values.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId);
-                values.put(AggregationExceptions.TYPE, exceptionType);
-                mResolver.update(AggregationExceptions.CONTENT_URI, values, null, null);
-            }
-        }
-    }
-
-    private void showOptionsActivity() {
-        final Intent intent = new Intent(this, ContactOptionsActivity.class);
-        intent.setData(mLookupUri);
-        startActivity(intent);
-    }
-
-    private ViewEntry getViewEntryForMenuItem(MenuItem item) {
-        AdapterView.AdapterContextMenuInfo info;
-        try {
-             info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
-        } catch (ClassCastException e) {
-            Log.e(TAG, "bad menuInfo", e);
-            return null;
-        }
-
-        return ContactEntryAdapter.getEntry(mSections, info.position, SHOW_SEPARATORS);
-    }
-
-    @Override
-    public boolean onKeyDown(int keyCode, KeyEvent event) {
-        switch (keyCode) {
-            case KeyEvent.KEYCODE_CALL: {
-                try {
-                    ITelephony phone = ITelephony.Stub.asInterface(
-                            ServiceManager.checkService("phone"));
-                    if (phone != null && !phone.isIdle()) {
-                        // Skip out and let the key be handled at a higher level
-                        break;
-                    }
-                } catch (RemoteException re) {
-                    // Fall through and try to call the contact
-                }
-
-                int index = mListView.getSelectedItemPosition();
-                if (index != -1) {
-                    ViewEntry entry = ViewAdapter.getEntry(mSections, index, SHOW_SEPARATORS);
-                    if (entry != null &&
-                            entry.intent.getAction() == Intent.ACTION_CALL_PRIVILEGED) {
-                        startActivity(entry.intent);
-                        return true;
-                    }
-                } else if (mPrimaryPhoneUri != null) {
-                    // There isn't anything selected, call the default number
-                    final Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
-                            mPrimaryPhoneUri);
-                    startActivity(intent);
-                    return true;
-                }
-                return false;
-            }
-
-            case KeyEvent.KEYCODE_DEL: {
-                if (mReadOnlySourcesCnt > 0 & mWritableSourcesCnt > 0) {
-                    showDialog(DIALOG_CONFIRM_READONLY_DELETE);
-                } else if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt == 0) {
-                    showDialog(DIALOG_CONFIRM_READONLY_HIDE);
-                } else if (mReadOnlySourcesCnt == 0 && mWritableSourcesCnt > 1) {
-                    showDialog(DIALOG_CONFIRM_MULTIPLE_DELETE);
-                } else {
-                    showDialog(DIALOG_CONFIRM_DELETE);
-                }
-                return true;
-            }
-        }
-
-        return super.onKeyDown(keyCode, event);
-    }
-
-    public void onItemClick(AdapterView parent, View v, int position, long id) {
-        ViewEntry entry = ViewAdapter.getEntry(mSections, position, SHOW_SEPARATORS);
-        if (entry != null) {
-            Intent intent = entry.intent;
-            if (intent != null) {
-                try {
-                    startActivity(intent);
-                } catch (ActivityNotFoundException e) {
-                    Log.e(TAG, "No activity found for intent: " + intent);
-                    signalError();
-                }
-            } else {
-                signalError();
-            }
-        } else {
-            signalError();
-        }
-    }
-
-    /**
-     * Signal an error to the user via a beep, or some other method.
-     */
-    private void signalError() {
-        //TODO: implement this when we have the sonification APIs
-    }
-
-    /**
-     * Build up the entries to display on the screen.
-     *
-     * @param personCursor the URI for the contact being displayed
-     */
-    private final void buildEntries() {
-        // Clear out the old entries
-        final int numSections = mSections.size();
-        for (int i = 0; i < numSections; i++) {
-            mSections.get(i).clear();
-        }
-
-        mRawContactIds.clear();
-
-        mReadOnlySourcesCnt = 0;
-        mWritableSourcesCnt = 0;
-        mAllRestricted = true;
-        mPrimaryPhoneUri = null;
-
-        mWritableRawContactIds.clear();
-
-        final Context context = this;
-        final Sources sources = Sources.getInstance(context);
-
-        // Build up method entries
-        if (mLookupUri != null) {
-            for (Entity entity: mEntities) {
-                final ContentValues entValues = entity.getEntityValues();
-                final String accountType = entValues.getAsString(RawContacts.ACCOUNT_TYPE);
-                final long rawContactId = entValues.getAsLong(RawContacts._ID);
-
-                // Mark when this contact has any unrestricted components
-                final boolean isRestricted = entValues.getAsInteger(RawContacts.IS_RESTRICTED) != 0;
-                if (!isRestricted) mAllRestricted = false;
-
-                if (!mRawContactIds.contains(rawContactId)) {
-                    mRawContactIds.add(rawContactId);
-                }
-                ContactsSource contactsSource = sources.getInflatedSource(accountType,
-                        ContactsSource.LEVEL_SUMMARY);
-                if (contactsSource != null && contactsSource.readOnly) {
-                    mReadOnlySourcesCnt += 1;
-                } else {
-                    mWritableSourcesCnt += 1;
-                    mWritableRawContactIds.add(rawContactId);
-                }
-
-
-                for (NamedContentValues subValue : entity.getSubValues()) {
-                    final ContentValues entryValues = subValue.values;
-                    entryValues.put(Data.RAW_CONTACT_ID, rawContactId);
-
-                    final long dataId = entryValues.getAsLong(Data._ID);
-                    final String mimeType = entryValues.getAsString(Data.MIMETYPE);
-                    if (mimeType == null) continue;
-
-                    final DataKind kind = sources.getKindOrFallback(accountType, mimeType, this,
-                            ContactsSource.LEVEL_MIMETYPES);
-                    if (kind == null) continue;
-
-                    final ViewEntry entry = ViewEntry.fromValues(context, mimeType, kind,
-                            rawContactId, dataId, entryValues);
-
-                    final boolean hasData = !TextUtils.isEmpty(entry.data);
-                    final boolean isSuperPrimary = entryValues.getAsInteger(
-                            Data.IS_SUPER_PRIMARY) != 0;
-
-                    if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
-                        // Build phone entries
-                        mNumPhoneNumbers++;
-
-                        entry.intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
-                                Uri.fromParts(Constants.SCHEME_TEL, entry.data, null));
-                        entry.secondaryIntent = new Intent(Intent.ACTION_SENDTO,
-                                Uri.fromParts(Constants.SCHEME_SMSTO, entry.data, null));
-
-                        // Remember super-primary phone
-                        if (isSuperPrimary) mPrimaryPhoneUri = entry.uri;
-
-                        entry.isPrimary = isSuperPrimary;
-                        mPhoneEntries.add(entry);
-
-                        if (entry.type == CommonDataKinds.Phone.TYPE_MOBILE
-                                || mShowSmsLinksForAllPhones) {
-                            // Add an SMS entry
-                            if (kind.iconAltRes > 0) {
-                                entry.secondaryActionIcon = kind.iconAltRes;
-                            }
-                        }
-                    } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
-                        // Build email entries
-                        entry.intent = new Intent(Intent.ACTION_SENDTO,
-                                Uri.fromParts(Constants.SCHEME_MAILTO, entry.data, null));
-                        entry.isPrimary = isSuperPrimary;
-                        mEmailEntries.add(entry);
-
-                        // When Email rows have status, create additional Im row
-                        final DataStatus status = mStatuses.get(entry.id);
-                        if (status != null) {
-                            final String imMime = Im.CONTENT_ITEM_TYPE;
-                            final DataKind imKind = sources.getKindOrFallback(accountType,
-                                    imMime, this, ContactsSource.LEVEL_MIMETYPES);
-                            final ViewEntry imEntry = ViewEntry.fromValues(context,
-                                    imMime, imKind, rawContactId, dataId, entryValues);
-                            imEntry.intent = ContactsUtils.buildImIntent(entryValues);
-                            imEntry.applyStatus(status, false);
-                            mImEntries.add(imEntry);
-                        }
-                    } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
-                        // Build postal entries
-                        entry.maxLines = 4;
-                        entry.intent = new Intent(Intent.ACTION_VIEW, entry.uri);
-                        mPostalEntries.add(entry);
-                    } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
-                        // Build IM entries
-                        entry.intent = ContactsUtils.buildImIntent(entryValues);
-                        if (TextUtils.isEmpty(entry.label)) {
-                            entry.label = getString(R.string.chat).toLowerCase();
-                        }
-
-                        // Apply presence and status details when available
-                        final DataStatus status = mStatuses.get(entry.id);
-                        if (status != null) {
-                            entry.applyStatus(status, false);
-                        }
-                        mImEntries.add(entry);
-                    } else if (Organization.CONTENT_ITEM_TYPE.equals(mimeType) &&
-                            (hasData || !TextUtils.isEmpty(entry.label))) {
-                        // Build organization entries
-                        final boolean isNameRawContact = (mNameRawContactId == rawContactId);
-
-                        final boolean duplicatesTitle =
-                            isNameRawContact
-                            && mDisplayNameSource == DisplayNameSources.ORGANIZATION
-                            && (!hasData || TextUtils.isEmpty(entry.label));
-
-                        if (!duplicatesTitle) {
-                            entry.uri = null;
-
-                            if (TextUtils.isEmpty(entry.label)) {
-                                entry.label = entry.data;
-                                entry.data = "";
-                            }
-
-                            mOrganizationEntries.add(entry);
-                        }
-                    } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
-                        // Build nickname entries
-                        final boolean isNameRawContact = (mNameRawContactId == rawContactId);
-
-                        final boolean duplicatesTitle =
-                            isNameRawContact
-                            && mDisplayNameSource == DisplayNameSources.NICKNAME;
-
-                        if (!duplicatesTitle) {
-                            entry.uri = null;
-                            mNicknameEntries.add(entry);
-                        }
-                    } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
-                        // Build note entries
-                        entry.uri = null;
-                        entry.maxLines = 100;
-                        mOtherEntries.add(entry);
-                    } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
-                        // Build note entries
-                        entry.uri = null;
-                        entry.maxLines = 10;
-                        try {
-                            WebAddress webAddress = new WebAddress(entry.data);
-                            entry.intent = new Intent(Intent.ACTION_VIEW,
-                                    Uri.parse(webAddress.toString()));
-                        } catch (ParseException e) {
-                            Log.e(TAG, "Couldn't parse website: " + entry.data);
-                        }
-                        mOtherEntries.add(entry);
-                    } else {
-                        // Handle showing custom rows
-                        entry.intent = new Intent(Intent.ACTION_VIEW, entry.uri);
-
-                        // Use social summary when requested by external source
-                        final DataStatus status = mStatuses.get(entry.id);
-                        final boolean hasSocial = kind.actionBodySocial && status != null;
-                        if (hasSocial) {
-                            entry.applyStatus(status, true);
-                        }
-
-                        if (hasSocial || hasData) {
-                            mOtherEntries.add(entry);
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    static String buildActionString(DataKind kind, ContentValues values, boolean lowerCase,
-            Context context) {
-        if (kind.actionHeader == null) {
-            return null;
-        }
-        CharSequence actionHeader = kind.actionHeader.inflateUsing(context, values);
-        if (actionHeader == null) {
-            return null;
-        }
-        return lowerCase ? actionHeader.toString().toLowerCase() : actionHeader.toString();
-    }
-
-    static String buildDataString(DataKind kind, ContentValues values, Context context) {
-        if (kind.actionBody == null) {
-            return null;
-        }
-        CharSequence actionBody = kind.actionBody.inflateUsing(context, values);
-        return actionBody == null ? null : actionBody.toString();
-    }
-
-    /**
-     * A basic structure with the data for a contact entry in the list.
-     */
-    static class ViewEntry extends ContactEntryAdapter.Entry implements Collapsible<ViewEntry> {
-        public Context context = null;
-        public String resPackageName = null;
-        public int actionIcon = -1;
-        public boolean isPrimary = false;
-        public int secondaryActionIcon = -1;
-        public Intent intent;
-        public Intent secondaryIntent = null;
-        public int maxLabelLines = 1;
-        public ArrayList<Long> ids = new ArrayList<Long>();
-        public int collapseCount = 0;
-
-        public int presence = -1;
-
-        public CharSequence footerLine = null;
-
-        private ViewEntry() {
-        }
-
-        /**
-         * Build new {@link ViewEntry} and populate from the given values.
-         */
-        public static ViewEntry fromValues(Context context, String mimeType, DataKind kind,
-                long rawContactId, long dataId, ContentValues values) {
-            final ViewEntry entry = new ViewEntry();
-            entry.context = context;
-            entry.contactId = rawContactId;
-            entry.id = dataId;
-            entry.uri = ContentUris.withAppendedId(Data.CONTENT_URI, entry.id);
-            entry.mimetype = mimeType;
-            entry.label = buildActionString(kind, values, false, context);
-            entry.data = buildDataString(kind, values, context);
-
-            if (kind.typeColumn != null && values.containsKey(kind.typeColumn)) {
-                entry.type = values.getAsInteger(kind.typeColumn);
-            }
-            if (kind.iconRes > 0) {
-                entry.resPackageName = kind.resPackageName;
-                entry.actionIcon = kind.iconRes;
-            }
-
-            return entry;
-        }
-
-        /**
-         * Apply given {@link DataStatus} values over this {@link ViewEntry}
-         *
-         * @param fillData When true, the given status replaces {@link #data}
-         *            and {@link #footerLine}. Otherwise only {@link #presence}
-         *            is updated.
-         */
-        public ViewEntry applyStatus(DataStatus status, boolean fillData) {
-            presence = status.getPresence();
-            if (fillData && status.isValid()) {
-                this.data = status.getStatus().toString();
-                this.footerLine = status.getTimestampLabel(context);
-            }
-
-            return this;
-        }
-
-        public boolean collapseWith(ViewEntry entry) {
-            // assert equal collapse keys
-            if (!shouldCollapseWith(entry)) {
-                return false;
-            }
-
-            // Choose the label associated with the highest type precedence.
-            if (TypePrecedence.getTypePrecedence(mimetype, type)
-                    > TypePrecedence.getTypePrecedence(entry.mimetype, entry.type)) {
-                type = entry.type;
-                label = entry.label;
-            }
-
-            // Choose the max of the maxLines and maxLabelLines values.
-            maxLines = Math.max(maxLines, entry.maxLines);
-            maxLabelLines = Math.max(maxLabelLines, entry.maxLabelLines);
-
-            // Choose the presence with the highest precedence.
-            if (StatusUpdates.getPresencePrecedence(presence)
-                    < StatusUpdates.getPresencePrecedence(entry.presence)) {
-                presence = entry.presence;
-            }
-
-            // If any of the collapsed entries are primary make the whole thing primary.
-            isPrimary = entry.isPrimary ? true : isPrimary;
-
-            // uri, and contactdId, shouldn't make a difference. Just keep the original.
-
-            // Keep track of all the ids that have been collapsed with this one.
-            ids.add(entry.id);
-            collapseCount++;
-            return true;
-        }
-
-        public boolean shouldCollapseWith(ViewEntry entry) {
-            if (entry == null) {
-                return false;
-            }
-
-            if (!ContactsUtils.shouldCollapse(context, mimetype, data, entry.mimetype,
-                    entry.data)) {
-                return false;
-            }
-
-            if (!TextUtils.equals(mimetype, entry.mimetype)
-                    || !ContactsUtils.areIntentActionEqual(intent, entry.intent)
-                    || !ContactsUtils.areIntentActionEqual(secondaryIntent, entry.secondaryIntent)
-                    || actionIcon != entry.actionIcon) {
-                return false;
-            }
-
-            return true;
-        }
-    }
-
-    /** Cache of the children views of a row */
-    static class ViewCache {
-        public TextView label;
-        public TextView data;
-        public TextView footer;
-        public ImageView actionIcon;
-        public ImageView presenceIcon;
-        public ImageView primaryIcon;
-        public ImageView secondaryActionButton;
-        public View secondaryActionDivider;
-
-        // Need to keep track of this too
-        ViewEntry entry;
-    }
-
-    private final class ViewAdapter extends ContactEntryAdapter<ViewEntry>
-            implements View.OnClickListener {
-
-
-        ViewAdapter(Context context, ArrayList<ArrayList<ViewEntry>> sections) {
-            super(context, sections, SHOW_SEPARATORS);
-        }
-
-        public void onClick(View v) {
-            Intent intent = (Intent) v.getTag();
-            startActivity(intent);
-        }
-
-        @Override
-        public View getView(int position, View convertView, ViewGroup parent) {
-            ViewEntry entry = getEntry(mSections, position, false);
-            View v;
-
-            ViewCache views;
-
-            // Check to see if we can reuse convertView
-            if (convertView != null) {
-                v = convertView;
-                views = (ViewCache) v.getTag();
-            } else {
-                // Create a new view if needed
-                v = mInflater.inflate(R.layout.list_item_text_icons, parent, false);
-
-                // Cache the children
-                views = new ViewCache();
-                views.label = (TextView) v.findViewById(android.R.id.text1);
-                views.data = (TextView) v.findViewById(android.R.id.text2);
-                views.footer = (TextView) v.findViewById(R.id.footer);
-                views.actionIcon = (ImageView) v.findViewById(R.id.action_icon);
-                views.primaryIcon = (ImageView) v.findViewById(R.id.primary_icon);
-                views.presenceIcon = (ImageView) v.findViewById(R.id.presence_icon);
-                views.secondaryActionButton = (ImageView) v.findViewById(
-                        R.id.secondary_action_button);
-                views.secondaryActionButton.setOnClickListener(this);
-                views.secondaryActionDivider = v.findViewById(R.id.divider);
-                v.setTag(views);
-            }
-
-            // Update the entry in the view cache
-            views.entry = entry;
-
-            // Bind the data to the view
-            bindView(v, entry);
-            return v;
-        }
-
-        @Override
-        protected View newView(int position, ViewGroup parent) {
-            // getView() handles this
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        protected void bindView(View view, ViewEntry entry) {
-            final Resources resources = mContext.getResources();
-            ViewCache views = (ViewCache) view.getTag();
-
-            // Set the label
-            TextView label = views.label;
-            setMaxLines(label, entry.maxLabelLines);
-            label.setText(entry.label);
-
-            // Set the data
-            TextView data = views.data;
-            if (data != null) {
-                if (entry.mimetype.equals(Phone.CONTENT_ITEM_TYPE)
-                        || entry.mimetype.equals(Constants.MIME_SMS_ADDRESS)) {
-                    data.setText(PhoneNumberUtils.formatNumber(entry.data));
-                } else {
-                    data.setText(entry.data);
-                }
-                setMaxLines(data, entry.maxLines);
-            }
-
-            // Set the footer
-            if (!TextUtils.isEmpty(entry.footerLine)) {
-                views.footer.setText(entry.footerLine);
-                views.footer.setVisibility(View.VISIBLE);
-            } else {
-                views.footer.setVisibility(View.GONE);
-            }
-
-            // Set the primary icon
-            views.primaryIcon.setVisibility(entry.isPrimary ? View.VISIBLE : View.GONE);
-
-            // Set the action icon
-            ImageView action = views.actionIcon;
-            if (entry.actionIcon != -1) {
-                Drawable actionIcon;
-                if (entry.resPackageName != null) {
-                    // Load external resources through PackageManager
-                    actionIcon = mContext.getPackageManager().getDrawable(entry.resPackageName,
-                            entry.actionIcon, null);
-                } else {
-                    actionIcon = resources.getDrawable(entry.actionIcon);
-                }
-                action.setImageDrawable(actionIcon);
-                action.setVisibility(View.VISIBLE);
-            } else {
-                // Things should still line up as if there was an icon, so make it invisible
-                action.setVisibility(View.INVISIBLE);
-            }
-
-            // Set the presence icon
-            Drawable presenceIcon = ContactPresenceIconUtil.getPresenceIcon(
-                    mContext, entry.presence);
-            ImageView presenceIconView = views.presenceIcon;
-            if (presenceIcon != null) {
-                presenceIconView.setImageDrawable(presenceIcon);
-                presenceIconView.setVisibility(View.VISIBLE);
-            } else {
-                presenceIconView.setVisibility(View.GONE);
-            }
-
-            // Set the secondary action button
-            ImageView secondaryActionView = views.secondaryActionButton;
-            Drawable secondaryActionIcon = null;
-            if (entry.secondaryActionIcon != -1) {
-                secondaryActionIcon = resources.getDrawable(entry.secondaryActionIcon);
-            }
-            if (entry.secondaryIntent != null && secondaryActionIcon != null) {
-                secondaryActionView.setImageDrawable(secondaryActionIcon);
-                secondaryActionView.setTag(entry.secondaryIntent);
-                secondaryActionView.setVisibility(View.VISIBLE);
-                views.secondaryActionDivider.setVisibility(View.VISIBLE);
-            } else {
-                secondaryActionView.setVisibility(View.GONE);
-                views.secondaryActionDivider.setVisibility(View.GONE);
-            }
-        }
-
-        private void setMaxLines(TextView textView, int maxLines) {
-            if (maxLines == 1) {
-                textView.setSingleLine(true);
-                textView.setEllipsize(TextUtils.TruncateAt.END);
-            } else {
-                textView.setSingleLine(false);
-                textView.setMaxLines(maxLines);
-                textView.setEllipsize(null);
-            }
-        }
-    }
-
-    private interface StatusQuery {
-        final String[] PROJECTION = new String[] {
-                Data._ID,
-                Data.STATUS,
-                Data.STATUS_RES_PACKAGE,
-                Data.STATUS_ICON,
-                Data.STATUS_LABEL,
-                Data.STATUS_TIMESTAMP,
-                Data.PRESENCE,
-        };
-
-        final int _ID = 0;
-    }
-
-    @Override
-    public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData,
-            boolean globalSearch) {
-        if (globalSearch) {
-            super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
-        } else {
-            ContactsSearchManager.startSearch(this, initialQuery);
-        }
-    }
-}
diff --git a/src/com/android/contacts/activities/ContactDetailActivity.java b/src/com/android/contacts/activities/ContactDetailActivity.java
new file mode 100644
index 0000000..f1a3c7e
--- /dev/null
+++ b/src/com/android/contacts/activities/ContactDetailActivity.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.activities;
+
+import com.android.contacts.ContactsSearchManager;
+import com.android.contacts.R;
+import com.android.contacts.views.detail.ContactDetailFragment;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+
+public class ContactDetailActivity extends Activity {
+    private static final String TAG = "ContactDetailActivity";
+
+    private ContactDetailFragment mFragment;
+
+    private final FragmentCallbackHandler mCallbackHandler = new FragmentCallbackHandler();
+
+    @Override
+    public void onCreate(Bundle savedState) {
+        super.onCreate(savedState);
+
+        setContentView(R.layout.contact_detail_activity);
+
+        Log.i(TAG, getIntent().getData().toString());
+
+        final View view = findViewById(R.id.contact_detail_fragment);
+        mFragment = (ContactDetailFragment) findFragmentById(R.id.contact_detail_fragment);
+        mFragment.setCallbacks(mCallbackHandler);
+        mFragment.loadUri(getIntent().getData());
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        // TODO: This is too hardwired.
+        if (mFragment.onCreateOptionsMenu(menu, getMenuInflater())) return true;
+
+        return super.onCreateOptionsMenu(menu);
+    }
+
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        // TODO: This is too hardwired.
+        if (mFragment.onPrepareOptionsMenu(menu)) return true;
+
+        return super.onPrepareOptionsMenu(menu);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        // TODO: This is too hardwired.
+        if (mFragment.onOptionsItemSelected(item)) return true;
+
+        return super.onOptionsItemSelected(item);
+    }
+
+    @Override
+    protected Dialog onCreateDialog(int id, Bundle args) {
+        // ask the Fragment whether it knows about the dialog
+        final Dialog fragmentResult = mFragment.onCreateDialog(id, args);
+        if (fragmentResult != null) return fragmentResult;
+
+        // Nobody knows about the Dialog
+        Log.w(TAG, "Unknown dialog requested, id: " + id + ", args: " + args);
+        return null;
+    }
+
+    @Override
+    public boolean onContextItemSelected(MenuItem item) {
+        // TODO: This is too hardwired.
+        if (mFragment.onContextItemSelected(item)) return true;
+
+        return super.onContextItemSelected(item);
+    }
+
+    @Override
+    public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData,
+            boolean globalSearch) {
+        if (globalSearch) {
+            super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
+        } else {
+            ContactsSearchManager.startSearch(this, initialQuery);
+        }
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        // TODO: This is too hardwired.
+        if (mFragment.onKeyDown(keyCode, event)) return true;
+
+        return super.onKeyDown(keyCode, event);
+    }
+
+    private class FragmentCallbackHandler implements ContactDetailFragment.Callbacks {
+        public void closeBecauseContactNotFound() {
+            finish();
+        }
+
+        public void editContact(Uri rawContactUri) {
+            startActivity(new Intent(Intent.ACTION_EDIT, rawContactUri));
+        }
+
+        public void itemClicked(Intent intent) {
+            startActivity(intent);
+        }
+    }
+}
diff --git a/src/com/android/contacts/activities/ContactEditActivity.java b/src/com/android/contacts/activities/ContactEditActivity.java
new file mode 100644
index 0000000..22aae18
--- /dev/null
+++ b/src/com/android/contacts/activities/ContactEditActivity.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.activities;
+
+import com.android.contacts.ContactsSearchManager;
+import com.android.contacts.R;
+import com.android.contacts.util.DialogManager;
+import com.android.contacts.views.edit.ContactEditFragment;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+
+public class ContactEditActivity extends Activity implements
+        DialogManager.DialogShowingViewActivity {
+
+    private static final String TAG = "ContactEditActivity";
+    private static final int DIALOG_VIEW_DIALOGS_ID1 = 1;
+    private static final int DIALOG_VIEW_DIALOGS_ID2 = 2;
+
+    private final FragmentCallbackHandler mCallbackHandler = new FragmentCallbackHandler();
+    private final DialogManager mDialogManager = new DialogManager(this, DIALOG_VIEW_DIALOGS_ID1,
+            DIALOG_VIEW_DIALOGS_ID2);
+
+    private ContactEditFragment mFragment;
+
+    @Override
+    public void onCreate(Bundle savedState) {
+        super.onCreate(savedState);
+
+        setContentView(R.layout.contact_edit_activity);
+
+        final Intent intent = getIntent();
+        final String action = intent.getAction();
+        final Uri uri = intent.getData();
+        final String mimeType = intent.resolveType(getContentResolver());
+        final Bundle intentExtras = intent.getExtras();
+
+        mFragment = (ContactEditFragment) findFragmentById(R.id.contact_edit_fragment);
+        mFragment.setCallbacks(mCallbackHandler);
+        mFragment.load(action, uri, mimeType, intentExtras);
+    }
+
+    private class FragmentCallbackHandler implements ContactEditFragment.Callbacks {
+        public void closeAfterRevert() {
+            finish();
+        }
+
+        public void closeAfterDelete() {
+            finish();
+        }
+
+        public void closeBecauseContactNotFound() {
+            finish();
+        }
+
+        public void closeAfterSplit() {
+            finish();
+        }
+
+        public void closeBecauseAccountSelectorAborted() {
+            finish();
+        }
+
+        public void setTitleTo(int resourceId) {
+            setTitle(resourceId);
+        }
+
+        public void closeAfterSaving(int resultCode, Intent resultIntent) {
+            setResult(resultCode, resultIntent);
+            finish();
+        }
+    }
+
+    public DialogManager getDialogManager() {
+        return mDialogManager;
+    }
+
+    @Override
+    protected Dialog onCreateDialog(int id, Bundle args) {
+        // If this is a dynamic dialog, use the DialogManager
+        if (id == DIALOG_VIEW_DIALOGS_ID1 || id == DIALOG_VIEW_DIALOGS_ID2) {
+            final Dialog dialog = mDialogManager.onCreateDialog(id, args);
+            if (dialog != null) return dialog;
+            return super.onCreateDialog(id, args);
+        }
+
+        // ask the Fragment whether it knows about the dialog
+        final Dialog fragmentResult = mFragment.onCreateDialog(id, args);
+        if (fragmentResult != null) return fragmentResult;
+
+        // Nobody knows about the Dialog
+        Log.w(TAG, "Unknown dialog requested, id: " + id + ", args: " + args);
+        return null;
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        // TODO: This is too hardwired.
+        mFragment.onActivityResult(requestCode, resultCode, data);
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        // TODO: This is too hardwired.
+        if (mFragment.onCreateOptionsMenu(menu, getMenuInflater())) return true;
+
+        return super.onCreateOptionsMenu(menu);
+    }
+
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        // TODO: This is too hardwired.
+        if (mFragment.onPrepareOptionsMenu(menu)) return true;
+
+        return super.onPrepareOptionsMenu(menu);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        // TODO: This is too hardwired.
+        if (mFragment.onOptionsItemSelected(item)) return true;
+
+        return super.onOptionsItemSelected(item);
+    }
+
+    @Override
+    public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData,
+            boolean globalSearch) {
+        if (globalSearch) {
+            super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
+        } else {
+            ContactsSearchManager.startSearch(this, initialQuery);
+        }
+    }
+}
diff --git a/src/com/android/contacts/activities/TwoPaneActivity.java b/src/com/android/contacts/activities/TwoPaneActivity.java
new file mode 100644
index 0000000..3b52156
--- /dev/null
+++ b/src/com/android/contacts/activities/TwoPaneActivity.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.activities;
+
+import com.android.contacts.R;
+import com.android.contacts.list.DefaultContactBrowseListFragment;
+import com.android.contacts.list.OnContactBrowserActionListener;
+import com.android.contacts.views.detail.ContactDetailFragment;
+import com.android.contacts.widget.SearchEditText;
+import com.android.contacts.widget.SearchEditText.OnFilterTextListener;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.widget.Toast;
+
+public class TwoPaneActivity extends Activity {
+    private final static String TAG = "TwoPaneActivity";
+    private DefaultContactBrowseListFragment mListFragment;
+    private ContactDetailFragment mDetailFragment;
+    private DetailCallbackHandler mDetailCallbackHandler = new DetailCallbackHandler();
+    private ListCallbackHandler mListCallbackHandler = new ListCallbackHandler();
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.two_pane_activity);
+
+        mListFragment = (DefaultContactBrowseListFragment) findFragmentById(R.id.two_pane_list);
+        mListFragment.setOnContactListActionListener(mListCallbackHandler);
+
+        mDetailFragment = (ContactDetailFragment) findFragmentById(R.id.two_pane_detail);
+        mDetailFragment.setCallbacks(mDetailCallbackHandler);
+
+        setupSearchUI();
+    }
+
+    private void setupSearchUI() {
+        SearchEditText searchEditText = (SearchEditText)findViewById(R.id.search_src_text);
+        searchEditText.setOnFilterTextListener(new OnFilterTextListener() {
+            public void onFilterChange(String queryString) {
+                mListFragment.setSearchMode(!TextUtils.isEmpty(queryString));
+                mListFragment.setQueryString(queryString);
+            }
+
+            public void onCancelSearch() {
+            }
+        });
+    }
+
+    private class ListCallbackHandler implements OnContactBrowserActionListener {
+        public void onAddToFavoritesAction(Uri contactUri) {
+            Toast.makeText(TwoPaneActivity.this, "onAddToFavoritesAction",
+                    Toast.LENGTH_LONG).show();
+        }
+
+        public void onCallContactAction(Uri contactUri) {
+            Toast.makeText(TwoPaneActivity.this, "onCallContactAction",
+                    Toast.LENGTH_LONG).show();
+        }
+
+        public void onCreateNewContactAction() {
+            Toast.makeText(TwoPaneActivity.this, "onCreateNewContactAction",
+                    Toast.LENGTH_LONG).show();
+        }
+
+        public void onDeleteContactAction(Uri contactUri) {
+            Toast.makeText(TwoPaneActivity.this, "onDeleteContactAction",
+                    Toast.LENGTH_LONG).show();
+        }
+
+        public void onEditContactAction(Uri contactLookupUri) {
+            Toast.makeText(TwoPaneActivity.this, "onEditContactAction",
+                    Toast.LENGTH_LONG).show();
+        }
+
+        public void onFinishAction() {
+            Toast.makeText(TwoPaneActivity.this, "onFinishAction",
+                    Toast.LENGTH_LONG).show();
+        }
+
+        public void onRemoveFromFavoritesAction(Uri contactUri) {
+            Toast.makeText(TwoPaneActivity.this, "onRemoveFromFavoritesAction",
+                    Toast.LENGTH_LONG).show();
+        }
+
+        public void onSearchAllContactsAction(String string) {
+            Toast.makeText(TwoPaneActivity.this, "onSearchAllContactsAction",
+                    Toast.LENGTH_LONG).show();
+        }
+
+        public void onSmsContactAction(Uri contactUri) {
+            Toast.makeText(TwoPaneActivity.this, "onSmsContactAction",
+                    Toast.LENGTH_LONG).show();
+        }
+
+        public void onViewContactAction(Uri contactLookupUri) {
+            mDetailFragment.loadUri(contactLookupUri);
+        }
+    }
+
+    private class DetailCallbackHandler implements ContactDetailFragment.Callbacks {
+        public void closeBecauseContactNotFound() {
+            Toast.makeText(TwoPaneActivity.this, "closeBecauseContactNotFound",
+                    Toast.LENGTH_LONG).show();
+        }
+
+        public void editContact(Uri rawContactUri) {
+            Toast.makeText(TwoPaneActivity.this, "editContact",
+                    Toast.LENGTH_LONG).show();
+        }
+
+        public void itemClicked(Intent intent) {
+            startActivity(intent);
+        }
+    }
+}
diff --git a/src/com/android/contacts/list/CallOrSmsInitiator.java b/src/com/android/contacts/list/CallOrSmsInitiator.java
new file mode 100644
index 0000000..b5aabee
--- /dev/null
+++ b/src/com/android/contacts/list/CallOrSmsInitiator.java
@@ -0,0 +1,139 @@
+/*
+ * 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.list;
+
+import com.android.contacts.ContactsUtils;
+import com.android.contacts.PhoneDisambigDialog;
+import com.android.internal.widget.RotarySelector.OnDialTriggerListener;
+
+import android.content.AsyncQueryHandler;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnDismissListener;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts.Data;
+
+/**
+ * Initiates phone calls or SMS messages.
+ */
+public class CallOrSmsInitiator {
+
+    private final Context mContext;
+    private AsyncQueryHandler mQueryHandler;
+    private int mCurrentToken;
+    private boolean mSendSms;
+
+    private static final String[] PHONE_NUMBER_PROJECTION = new String[] {
+            Phone._ID,
+            Phone.NUMBER,
+            Phone.IS_SUPER_PRIMARY,
+            RawContacts.ACCOUNT_TYPE,
+            Phone.TYPE,
+            Phone.LABEL
+    };
+
+    private static final String PHONE_NUMBER_SELECTION = Data.MIMETYPE + "='"
+            + Phone.CONTENT_ITEM_TYPE + "' AND " + Phone.NUMBER + " NOT NULL";
+    private OnDismissListener mDismissListener;
+
+    public CallOrSmsInitiator(Context context) {
+        this.mContext = context;
+        mQueryHandler = new AsyncQueryHandler(context.getContentResolver()) {
+            @Override
+            protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+                onPhoneNumberQueryComplete(token, cookie, cursor);
+            }
+        };
+    }
+
+    public void setOnDismissListener(DialogInterface.OnDismissListener dismissListener) {
+        this.mDismissListener = dismissListener;
+    }
+
+    protected void onPhoneNumberQueryComplete(int token, Object cookie, Cursor cursor) {
+        if (cursor == null || cursor.getCount() == 0) {
+            cursor.close();
+            return;
+        }
+
+        if (token != mCurrentToken) { // Stale query, ignore
+            cursor.close();
+            return;
+        }
+
+        String phone = null;
+        if (cursor.getCount() == 1) {
+            // only one number, call it.
+            cursor.moveToFirst();
+            phone = cursor.getString(cursor.getColumnIndex(Phone.NUMBER));
+        } else {
+            cursor.moveToPosition(-1);
+            while (cursor.moveToNext()) {
+                if (cursor.getInt(cursor.getColumnIndex(Phone.IS_SUPER_PRIMARY)) != 0) {
+                    // Found super primary, call it.
+                    phone = cursor.getString(cursor.getColumnIndex(Phone.NUMBER));
+                    break;
+                }
+            }
+        }
+
+        if (phone == null) {
+            // Display dialog to choose a number to call.
+            PhoneDisambigDialog phoneDialog = new PhoneDisambigDialog(mContext, cursor, mSendSms);
+            if (mDismissListener != null) {
+                phoneDialog.setOnDismissListener(mDismissListener);
+            }
+            phoneDialog.show();
+        } else {
+            if (mSendSms) {
+                ContactsUtils.initiateSms(mContext, phone);
+            } else {
+                ContactsUtils.initiateCall(mContext, phone);
+            }
+            if (mDismissListener != null) {
+                mDismissListener.onDismiss(null);
+            }
+        }
+    }
+
+    /**
+     * Initiates a phone call with the specified contact. If necessary, displays
+     * a disambiguation dialog to see which number to call.
+     */
+    public void initiateCall(Uri contactUri) {
+        callOrSendSms(contactUri, false);
+    }
+
+    /**
+     * Initiates a text message to the specified contact. If necessary, displays
+     * a disambiguation dialog to see which number to call.
+     */
+    public void initiateSms(Uri contactUri) {
+        callOrSendSms(contactUri, true);
+    }
+
+    private void callOrSendSms(Uri contactUri, boolean sendSms) {
+        mCurrentToken++;
+        mSendSms = sendSms;
+        Uri dataUri = Uri.withAppendedPath(contactUri, Contacts.Data.CONTENT_DIRECTORY);
+        mQueryHandler.startQuery(mCurrentToken, dataUri, dataUri, PHONE_NUMBER_PROJECTION,
+                PHONE_NUMBER_SELECTION, null, null);
+    }
+}
diff --git a/src/com/android/contacts/list/ContactBrowseListContextMenuAdapter.java b/src/com/android/contacts/list/ContactBrowseListContextMenuAdapter.java
new file mode 100644
index 0000000..617c855
--- /dev/null
+++ b/src/com/android/contacts/list/ContactBrowseListContextMenuAdapter.java
@@ -0,0 +1,139 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+import com.android.contacts.widget.ContextMenuAdapter;
+
+import android.net.Uri;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.AdapterView;
+
+/**
+ * A contextual menu adapter for the basic contact list.
+ */
+public class ContactBrowseListContextMenuAdapter implements ContextMenuAdapter {
+
+    private static final int MENU_ITEM_VIEW_CONTACT = 1;
+    private static final int MENU_ITEM_CALL = 2;
+    private static final int MENU_ITEM_SEND_SMS = 3;
+    private static final int MENU_ITEM_EDIT = 4;
+    private static final int MENU_ITEM_DELETE = 5;
+    private static final int MENU_ITEM_TOGGLE_STAR = 6;
+
+    private static final String TAG = "LightContactBrowserContextMenuAdapter";
+
+    private final ContactBrowseListFragment mContactListFragment;
+
+    public ContactBrowseListContextMenuAdapter(ContactBrowseListFragment fragment) {
+        this.mContactListFragment = fragment;
+    }
+
+    public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
+        AdapterView.AdapterContextMenuInfo info;
+        try {
+             info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+        } catch (ClassCastException e) {
+            Log.wtf(TAG, "Bad menuInfo", e);
+            return;
+        }
+
+        ContactListAdapter adapter = mContactListFragment.getAdapter();
+        int headerViewsCount = mContactListFragment.getListView().getHeaderViewsCount();
+        adapter.moveToPosition(info.position - headerViewsCount);
+
+        // Setup the menu header
+        menu.setHeaderTitle(adapter.getContactDisplayName());
+
+        // View contact details
+        menu.add(0, MENU_ITEM_VIEW_CONTACT, 0, R.string.menu_viewContact);
+
+        if (adapter.getHasPhoneNumber()) {
+            // Calling contact
+            menu.add(0, MENU_ITEM_CALL, 0, R.string.menu_call);
+            // Send SMS item
+            menu.add(0, MENU_ITEM_SEND_SMS, 0, R.string.menu_sendSMS);
+        }
+
+        // Star toggling
+        if (!adapter.isContactStarred()) {
+            menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_addStar);
+        } else {
+            menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_removeStar);
+        }
+
+        // Contact editing
+        menu.add(0, MENU_ITEM_EDIT, 0, R.string.menu_editContact);
+        menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_deleteContact);
+    }
+
+    public boolean onContextItemSelected(MenuItem item) {
+        AdapterView.AdapterContextMenuInfo info;
+        try {
+             info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
+        } catch (ClassCastException e) {
+            Log.wtf(TAG, "Bad menuInfo", e);
+            return false;
+        }
+
+        ContactListAdapter adapter = mContactListFragment.getAdapter();
+        int headerViewsCount = mContactListFragment.getListView().getHeaderViewsCount();
+        adapter.moveToPosition(info.position - headerViewsCount);
+
+        final Uri contactUri = adapter.getContactUri();
+        switch (item.getItemId()) {
+            case MENU_ITEM_VIEW_CONTACT: {
+                mContactListFragment.viewContact(contactUri);
+                return true;
+            }
+
+            case MENU_ITEM_TOGGLE_STAR: {
+                if (adapter.isContactStarred()) {
+                    mContactListFragment.removeFromFavorites(contactUri);
+                } else {
+                    mContactListFragment.addToFavorites(contactUri);
+                }
+                return true;
+            }
+
+            case MENU_ITEM_CALL: {
+                mContactListFragment.callContact(contactUri);
+                return true;
+            }
+
+            case MENU_ITEM_SEND_SMS: {
+                mContactListFragment.smsContact(contactUri);
+                return true;
+            }
+
+            case MENU_ITEM_EDIT: {
+                mContactListFragment.editContact(contactUri);
+                return true;
+            }
+
+            case MENU_ITEM_DELETE: {
+                mContactListFragment.deleteContact(contactUri);
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/src/com/android/contacts/list/ContactBrowseListFragment.java b/src/com/android/contacts/list/ContactBrowseListFragment.java
new file mode 100644
index 0000000..05f2364
--- /dev/null
+++ b/src/com/android/contacts/list/ContactBrowseListFragment.java
@@ -0,0 +1,97 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+
+import android.net.Uri;
+
+/**
+ * Fragment containing a contact list used for browsing (as compared to
+ * picking a contact with one of the PICK intents).
+ */
+public abstract class ContactBrowseListFragment extends
+        ContactEntryListFragment<ContactListAdapter> {
+
+    private OnContactBrowserActionListener mListener;
+
+    @Override
+    protected void prepareEmptyView() {
+        if (isSearchMode()) {
+            return;
+        } else if (isSearchResultsMode()) {
+            setEmptyText(R.string.noMatchingContacts);
+        } else if (isSyncActive()) {
+            if (hasIccCard()) {
+                setEmptyText(R.string.noContactsHelpTextWithSync);
+            } else {
+                setEmptyText(R.string.noContactsNoSimHelpTextWithSync);
+            }
+        } else {
+            if (hasIccCard()) {
+                setEmptyText(R.string.noContactsHelpText);
+            } else {
+                setEmptyText(R.string.noContactsNoSimHelpText);
+            }
+        }
+    }
+
+    public void setOnContactListActionListener(OnContactBrowserActionListener listener) {
+        mListener = listener;
+    }
+
+    public void createNewContact() {
+        mListener.onCreateNewContactAction();
+    }
+
+    public void searchAllContacts() {
+        mListener.onSearchAllContactsAction((String)null);
+    }
+
+    public void viewContact(Uri contactUri) {
+        mListener.onViewContactAction(contactUri);
+    }
+
+    public void editContact(Uri contactUri) {
+        mListener.onEditContactAction(contactUri);
+    }
+
+    public void deleteContact(Uri contactUri) {
+        mListener.onDeleteContactAction(contactUri);
+    }
+
+    public void addToFavorites(Uri contactUri) {
+        mListener.onAddToFavoritesAction(contactUri);
+    }
+
+    public void removeFromFavorites(Uri contactUri) {
+        mListener.onRemoveFromFavoritesAction(contactUri);
+    }
+
+    public void callContact(Uri contactUri) {
+        mListener.onCallContactAction(contactUri);
+    }
+
+    public void smsContact(Uri contactUri) {
+        mListener.onSmsContactAction(contactUri);
+    }
+
+    @Override
+    protected void finish() {
+        super.finish();
+        mListener.onFinishAction();
+    }
+}
diff --git a/src/com/android/contacts/list/ContactEntryListAdapter.java b/src/com/android/contacts/list/ContactEntryListAdapter.java
new file mode 100644
index 0000000..b6d8169
--- /dev/null
+++ b/src/com/android/contacts/list/ContactEntryListAdapter.java
@@ -0,0 +1,261 @@
+/*
+ * 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.list;
+
+import com.android.contacts.ContactPhotoLoader;
+import com.android.contacts.ContactsSectionIndexer;
+import com.android.contacts.R;
+import com.android.contacts.widget.PinnedHeaderListAdapter;
+import com.android.contacts.widget.TextWithHighlightingFactory;
+
+import android.app.patterns.CursorLoader;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.provider.ContactsContract.ContactCounts;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Common base class for various contact-related lists, e.g. contact list, phone number list
+ * etc.
+ */
+public abstract class ContactEntryListAdapter extends PinnedHeaderListAdapter {
+
+    /**
+     * The animation is used here to allocate animated name text views.
+     */
+    private TextWithHighlightingFactory mTextWithHighlightingFactory;
+
+    private int mDisplayOrder;
+    private int mSortOrder;
+    private boolean mNameHighlightingEnabled;
+    private boolean mSectionHeaderDisplayEnabled;
+
+    private boolean mDisplayPhotos;
+    private ContactPhotoLoader mPhotoLoader;
+
+    private String mQueryString;
+    private boolean mSearchMode;
+    private boolean mSearchResultsMode;
+
+    private boolean mLoading = true;
+    private boolean mEmptyListEnabled = true;
+
+    public ContactEntryListAdapter(Context context) {
+        super(context, R.layout.list_section, R.id.header_text,
+                context.getResources().getColor(R.color.pinned_header_background));
+    }
+
+    public Context getContext() {
+        return mContext;
+    }
+
+    public abstract String getContactDisplayName();
+    public abstract void configureLoader(CursorLoader loader);
+
+    public boolean isSearchMode() {
+        return mSearchMode;
+    }
+
+    public void setSearchMode(boolean flag) {
+        mSearchMode = flag;
+    }
+
+    public boolean isSearchResultsMode() {
+        return mSearchResultsMode;
+    }
+
+    public void setSearchResultsMode(boolean searchResultsMode) {
+        mSearchResultsMode = searchResultsMode;
+    }
+
+    public String getQueryString() {
+        return mQueryString;
+    }
+
+    public void setQueryString(String queryString) {
+        mQueryString = queryString;
+    }
+
+    public boolean isSectionHeaderDisplayEnabled() {
+        return mSectionHeaderDisplayEnabled;
+    }
+
+    public void setSectionHeaderDisplayEnabled(boolean flag) {
+        mSectionHeaderDisplayEnabled = flag;
+    }
+
+    public int getContactNameDisplayOrder() {
+        return mDisplayOrder;
+    }
+
+    public void setContactNameDisplayOrder(int displayOrder) {
+        mDisplayOrder = displayOrder;
+    }
+
+    public int getSortOrder() {
+        return mSortOrder;
+    }
+
+    public void setSortOrder(int sortOrder) {
+        mSortOrder = sortOrder;
+    }
+
+    // TODO no highlighting in STREQUENT mode
+    public void setNameHighlightingEnabled(boolean flag) {
+        mNameHighlightingEnabled = flag;
+    }
+
+    public boolean isNameHighlightingEnabled() {
+        return mNameHighlightingEnabled;
+    }
+
+    public void setTextWithHighlightingFactory(TextWithHighlightingFactory factory) {
+        mTextWithHighlightingFactory = factory;
+    }
+
+    protected TextWithHighlightingFactory getTextWithHighlightingFactory() {
+        return mTextWithHighlightingFactory;
+    }
+
+    public void setPhotoLoader(ContactPhotoLoader photoLoader) {
+        mPhotoLoader = photoLoader;
+    }
+
+    protected ContactPhotoLoader getPhotoLoader() {
+        return mPhotoLoader;
+    }
+
+    public boolean getDisplayPhotos() {
+        return mDisplayPhotos;
+    }
+
+    public void setDisplayPhotos(boolean displayPhotos) {
+        mDisplayPhotos = displayPhotos;
+    }
+
+    public boolean isEmptyListEnabled() {
+        return mEmptyListEnabled;
+    }
+
+    public void setEmptyListEnabled(boolean flag) {
+        mEmptyListEnabled = flag;
+    }
+
+    /*
+     * TODO change this method when loaders are introduced.
+     */
+    @Override
+    @Deprecated
+    public void onContentChanged() {
+        super.onContentChanged();
+    }
+
+    @Override
+    public void changeCursor(Cursor cursor) {
+        mLoading = false;
+        super.changeCursor(cursor);
+
+        if (isSectionHeaderDisplayEnabled()) {
+            updateIndexer(cursor);
+        }
+    }
+
+    /**
+     * Updates the indexer, which is used to produce section headers.
+     */
+    private void updateIndexer(Cursor cursor) {
+        if (cursor == null) {
+            setIndexer(null);
+            return;
+        }
+
+        Bundle bundle = cursor.getExtras();
+        if (bundle.containsKey(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES)) {
+            String sections[] =
+                    bundle.getStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES);
+            int counts[] = bundle.getIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS);
+            setIndexer(new ContactsSectionIndexer(sections, counts));
+        } else {
+            setIndexer(null);
+        }
+    }
+
+    @Override
+    public boolean isEmpty() {
+        // TODO
+//        if (contactsListActivity.mProviderStatus != ProviderStatus.STATUS_NORMAL) {
+//            return true;
+//        }
+
+        if (!mEmptyListEnabled) {
+            return false;
+        } else if (isSearchMode()) {
+            return TextUtils.isEmpty(getQueryString());
+        } else if (mLoading) {
+            // We don't want the empty state to show when loading.
+            return false;
+        } else {
+            return super.isEmpty();
+        }
+    }
+
+    public void moveToPosition(int position) {
+        // For side-effect
+        getItem(position);
+    }
+
+    @Override
+    public int getCount() {
+        if (!mDataValid) {
+            return 0;
+        }
+
+        int count = super.getCount();
+
+        if (mSearchMode) {
+            // Last element in the list is "Search all contacts"
+            count++;
+        }
+
+        return count;
+    }
+
+    public boolean isSearchAllContactsItemPosition(int position) {
+        return isSearchMode() && position == getCount() - 1;
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        if (isSearchAllContactsItemPosition(position)) {
+            return IGNORE_ITEM_VIEW_TYPE;
+        }
+
+        return 0;
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        if (isSearchAllContactsItemPosition(position)) {
+            return LayoutInflater.from(getContext()).inflate(
+                    R.layout.contacts_list_search_all_item, parent, false);
+        }
+        return super.getView(position, convertView, parent);
+    }
+}
diff --git a/src/com/android/contacts/list/ContactEntryListFragment.java b/src/com/android/contacts/list/ContactEntryListFragment.java
new file mode 100644
index 0000000..bb8484d
--- /dev/null
+++ b/src/com/android/contacts/list/ContactEntryListFragment.java
@@ -0,0 +1,707 @@
+/*
+ * 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.list;
+
+import com.android.contacts.ContactEntryListView;
+import com.android.contacts.ContactListEmptyView;
+import com.android.contacts.ContactPhotoLoader;
+import com.android.contacts.R;
+import com.android.contacts.ui.ContactsPreferences;
+import com.android.contacts.widget.ContextMenuAdapter;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.app.Activity;
+import android.app.patterns.CursorLoader;
+import android.app.patterns.Loader;
+import android.app.patterns.LoaderManagingFragment;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.IContentService;
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.provider.Settings;
+import android.provider.ContactsContract.ProviderStatus;
+import android.telephony.TelephonyManager;
+import android.text.Editable;
+import android.text.Html;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+import android.view.View.OnFocusChangeListener;
+import android.view.View.OnTouchListener;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView.OnItemClickListener;
+
+/**
+ * Common base class for various contact-related list fragments.
+ */
+public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter>
+        extends LoaderManagingFragment<Cursor>
+        implements OnItemClickListener, OnScrollListener, OnFocusChangeListener, OnTouchListener {
+
+    private static final String TAG = "ContactEntryListFragment";
+
+    private static final String LIST_STATE_KEY = "liststate";
+
+    private boolean mSectionHeaderDisplayEnabled;
+    private boolean mPhotoLoaderEnabled;
+    private boolean mSearchMode;
+    private boolean mSearchResultsMode;
+    private String mQueryString;
+
+    private CursorLoader mLoader;
+    private T mAdapter;
+    private View mView;
+    private ListView mListView;
+
+    /**
+     * Used for keeping track of the scroll state of the list.
+     */
+    private Parcelable mListState;
+
+    private boolean mLegacyCompatibility;
+    private int mDisplayOrder;
+    private int mSortOrder;
+
+    private ContextMenuAdapter mContextMenuAdapter;
+    private ContactPhotoLoader mPhotoLoader;
+    private ContactListEmptyView mEmptyView;
+    private ProviderStatusLoader mProviderStatusLoader;
+    private ContactsPreferences mContactsPrefs;
+
+    private int mProviderStatus = ProviderStatus.STATUS_NORMAL;
+
+    protected abstract View inflateView(LayoutInflater inflater, ViewGroup container);
+    protected abstract T createListAdapter();
+
+    /**
+     * @param position Please note that the position is already adjusted for
+     *            header views, so "0" means the first list item below header
+     *            views.
+     */
+    protected abstract void onItemClick(int position, long id);
+
+    public CursorLoader getLoader() {
+        return mLoader;
+    }
+
+    public T getAdapter() {
+        return mAdapter;
+    }
+
+    @Override
+    public View getView() {
+        return mView;
+    }
+
+    public ListView getListView() {
+        return mListView;
+    }
+
+    public ContactListEmptyView getEmptyView() {
+        return mEmptyView;
+    }
+
+    @Override
+    protected Loader<Cursor> onCreateLoader(int id, Bundle args) {
+        return new CursorLoader(getActivity(), null, null, null, null, null);
+    }
+
+    @Override
+    protected void onInitializeLoaders() {
+        mLoader = (CursorLoader)startLoading(0, null);
+        if (mProviderStatusLoader == null) {
+            mProviderStatusLoader = new ProviderStatusLoader(mLoader);
+        }
+    }
+
+    @Override
+    protected void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+        if (!checkProviderStatus(false)) {
+            return;
+        }
+
+        if (mEmptyView != null && (data == null || data.getCount() == 0)) {
+            prepareEmptyView();
+        }
+
+        mAdapter.changeCursor(data);
+
+        if (data != null) {
+            showCount(data);
+            completeRestoreInstanceState();
+        }
+    }
+
+    protected void reloadData() {
+        configureAdapter();
+        mAdapter.configureLoader(mLoader);
+        mLoader.forceLoad();
+    }
+
+    /**
+     * Configures the empty view. It is called when we are about to populate
+     * the list with an empty cursor.
+     */
+    protected void prepareEmptyView() {
+    }
+
+    /**
+     * Shows the count of entries included in the list. The default
+     * implementation does nothing.
+     */
+    protected void showCount(Cursor data) {
+    }
+
+    /**
+     * Provides logic that dismisses this fragment. The default implementation
+     * does nothing.
+     */
+    protected void finish() {
+    }
+
+    public void setSectionHeaderDisplayEnabled(boolean flag) {
+        mSectionHeaderDisplayEnabled = flag;
+    }
+
+    public boolean isSectionHeaderDisplayEnabled() {
+        return mSectionHeaderDisplayEnabled && !mSearchMode;
+    }
+
+    public void setPhotoLoaderEnabled(boolean flag) {
+        mPhotoLoaderEnabled = flag;
+        configurePhotoLoader();
+    }
+
+    public boolean isPhotoLoaderEnabled() {
+        return mPhotoLoaderEnabled;
+    }
+
+    public void setSearchMode(boolean flag) {
+        mSearchMode = flag;
+        if (mAdapter != null) {
+            mAdapter.setSearchMode(flag);
+        }
+    }
+
+    public boolean isSearchMode() {
+        return mSearchMode;
+    }
+
+    public void setSearchResultsMode(boolean flag) {
+        mSearchResultsMode = flag;
+        if (mAdapter != null) {
+            mAdapter.setSearchResultsMode(flag);
+        }
+    }
+
+    public boolean isSearchResultsMode() {
+        return mSearchResultsMode;
+    }
+
+    public String getQueryString() {
+        return mQueryString;
+    }
+
+    public void setQueryString(String queryString) {
+        if (!TextUtils.equals(mQueryString, queryString)) {
+            mQueryString = queryString;
+            if (mAdapter != null) {
+                mAdapter.setQueryString(queryString);
+                reloadData();
+            }
+        }
+    }
+
+    public boolean isLegacyCompatibilityMode() {
+        return mLegacyCompatibility;
+    }
+
+    public void setLegacyCompatibilityMode(boolean flag) {
+        mLegacyCompatibility = flag;
+    }
+
+    public int getContactNameDisplayOrder() {
+        return mDisplayOrder;
+    }
+
+    public void setContactNameDisplayOrder(int displayOrder) {
+        mDisplayOrder = displayOrder;
+        if (mAdapter != null) {
+            mAdapter.setContactNameDisplayOrder(displayOrder);
+        }
+    }
+
+    public int getSortOrder() {
+        return mSortOrder;
+    }
+
+    public void setSortOrder(int sortOrder) {
+        mSortOrder = sortOrder;
+        if (mAdapter != null) {
+            mAdapter.setSortOrder(sortOrder);
+        }
+    }
+
+    public void setContextMenuAdapter(ContextMenuAdapter adapter) {
+        mContextMenuAdapter = adapter;
+        if (mListView != null) {
+            mListView.setOnCreateContextMenuListener(adapter);
+        }
+    }
+
+    public ContextMenuAdapter getContextMenuAdapter() {
+        return mContextMenuAdapter;
+    }
+
+    @Override
+    public void onStart() {
+        if (mContactsPrefs == null) {
+            mContactsPrefs = new ContactsPreferences(getActivity());
+        }
+
+        loadPreferences(mContactsPrefs);
+        configureAdapter();
+        mAdapter.configureLoader(mLoader);
+
+        ContactEntryListView listView = (ContactEntryListView)mListView;
+        listView.setHighlightNamesWhenScrolling(isNameHighlighingEnabled());
+
+        super.onStart();
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+        configurePhotoLoader();
+    }
+
+    protected void loadPreferences(ContactsPreferences contactsPrefs) {
+        setContactNameDisplayOrder(contactsPrefs.getDisplayOrder());
+        setSortOrder(contactsPrefs.getSortOrder());
+    }
+
+    @Override
+    public void onCreate(Bundle savedState) {
+        super.onCreate(savedState);
+        // Retrieve list state. This will be applied in onLoadFinished
+        if (savedState != null) {
+            mListState = savedState.getParcelable(LIST_STATE_KEY);
+        }
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        mView = createView(inflater, container);
+        mAdapter = createListAdapter();
+        mAdapter.setSearchMode(isSearchMode());
+        mAdapter.setSearchResultsMode(isSearchResultsMode());
+        mAdapter.setPhotoLoader(mPhotoLoader);
+        mListView.setAdapter(mAdapter);
+
+        return mView;
+    }
+
+    protected View createView(LayoutInflater inflater, ViewGroup container) {
+        mView = inflateView(inflater, container);
+
+        mListView = (ListView)mView.findViewById(android.R.id.list);
+        if (mListView == null) {
+            throw new RuntimeException(
+                    "Your content must have a ListView whose id attribute is " +
+                    "'android.R.id.list'");
+        }
+
+        View emptyView = mView.findViewById(com.android.internal.R.id.empty);
+        if (emptyView != null) {
+            mListView.setEmptyView(emptyView);
+            if (emptyView instanceof ContactListEmptyView) {
+                mEmptyView = (ContactListEmptyView)emptyView;
+            }
+        }
+
+        mListView.setOnItemClickListener(this);
+        mListView.setOnFocusChangeListener(this);
+        mListView.setOnTouchListener(this);
+
+        // Tell list view to not show dividers. We'll do it ourself so that we can *not* show
+        // them when an A-Z headers is visible.
+        mListView.setDividerHeight(0);
+
+        // We manually save/restore the listview state
+        mListView.setSaveEnabled(false);
+
+        if (mContextMenuAdapter != null) {
+            mListView.setOnCreateContextMenuListener(mContextMenuAdapter);
+        }
+
+        configurePhotoLoader();
+        configureSearchResultText();
+        return mView;
+    }
+
+    protected void configurePhotoLoader() {
+        Activity activity = getActivity();
+        if (isPhotoLoaderEnabled() && activity != null) {
+            if (mPhotoLoader == null) {
+                mPhotoLoader = new ContactPhotoLoader(activity, R.drawable.ic_contact_list_picture);
+            }
+            if (mListView != null) {
+                mListView.setOnScrollListener(this);
+            }
+            if (mAdapter != null) {
+                mAdapter.setPhotoLoader(mPhotoLoader);
+            }
+        }
+    }
+
+    protected void configureSearchResultText() {
+        if (isSearchResultsMode() && mView != null) {
+            TextView titleText = (TextView)mView.findViewById(R.id.search_results_for);
+            if (titleText != null) {
+                titleText.setText(Html.fromHtml(getActivity().getString(R.string.search_results_for,
+                        "<b>" + getQueryString() + "</b>")));
+            }
+        }
+    }
+
+    protected void configureAdapter() {
+        if (mAdapter != null) {
+            mAdapter.setQueryString(mQueryString);
+            mAdapter.setContactNameDisplayOrder(mDisplayOrder);
+            mAdapter.setSortOrder(mSortOrder);
+            mAdapter.setNameHighlightingEnabled(isNameHighlighingEnabled());
+        }
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        mAdapter.changeCursor(null);
+    }
+
+    private boolean isNameHighlighingEnabled() {
+        // When sort order and display order contradict each other, we want to
+        // highlight the part of the name used for sorting.
+        if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_PRIMARY &&
+                mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_ALTERNATIVE) {
+            return true;
+        } else if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_ALTERNATIVE &&
+                mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+            int totalItemCount) {
+    }
+
+    public void onScrollStateChanged(AbsListView view, int scrollState) {
+        if (scrollState == OnScrollListener.SCROLL_STATE_FLING) {
+            mPhotoLoader.pause();
+        } else if (isPhotoLoaderEnabled()) {
+            mPhotoLoader.resume();
+        }
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+
+        registerProviderStatusObserver();
+
+        if (isPhotoLoaderEnabled()) {
+            mPhotoLoader.resume();
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        if (isPhotoLoaderEnabled()) {
+            mPhotoLoader.stop();
+        }
+        super.onDestroy();
+    }
+
+    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+        hideSoftKeyboard();
+
+        int adjPosition = position - mListView.getHeaderViewsCount();
+        if (adjPosition >= 0) {
+            onItemClick(adjPosition, id);
+        }
+    }
+
+    private void hideSoftKeyboard() {
+        // Hide soft keyboard, if visible
+        InputMethodManager inputMethodManager = (InputMethodManager)
+                getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+        inputMethodManager.hideSoftInputFromWindow(mListView.getWindowToken(), 0);
+    }
+
+    /**
+     * Dismisses the soft keyboard when the list takes focus.
+     */
+    public void onFocusChange(View view, boolean hasFocus) {
+        if (view == mListView && hasFocus) {
+            hideSoftKeyboard();
+        }
+    }
+
+    /**
+     * Dismisses the soft keyboard when the list is touched.
+     */
+    public boolean onTouch(View view, MotionEvent event) {
+        if (view == mListView) {
+            hideSoftKeyboard();
+        }
+        return false;
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        unregisterProviderStatusObserver();
+    }
+
+    /**
+     * Dismisses the search UI along with the keyboard if the filter text is empty.
+     */
+    public void onClose() {
+        hideSoftKeyboard();
+        finish();
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle icicle) {
+        super.onSaveInstanceState(icicle);
+        if (mListView != null) {
+            mListState = mListView.onSaveInstanceState();
+            icicle.putParcelable(LIST_STATE_KEY, mListState);
+        }
+    }
+
+    /**
+     * Restore the list state after the adapter is populated.
+     */
+    private void completeRestoreInstanceState() {
+        if (mListState != null) {
+            mListView.onRestoreInstanceState(mListState);
+            mListState = null;
+        }
+    }
+
+    private ContentObserver mProviderStatusObserver = new ContentObserver(new Handler()) {
+        @Override
+        public void onChange(boolean selfChange) {
+            checkProviderStatus(true);
+        }
+    };
+
+    /**
+     * Register an observer for provider status changes - we will need to
+     * reflect them in the UI.
+     */
+    private void registerProviderStatusObserver() {
+        getActivity().getContentResolver().registerContentObserver(ProviderStatus.CONTENT_URI,
+                false, mProviderStatusObserver);
+    }
+
+    /**
+     * Register an observer for provider status changes - we will need to
+     * reflect them in the UI.
+     */
+    private void unregisterProviderStatusObserver() {
+        getActivity().getContentResolver().unregisterContentObserver(mProviderStatusObserver);
+    }
+
+    /**
+     * Obtains the contacts provider status and configures the UI accordingly.
+     *
+     * @param loadData true if the method needs to start a query when the
+     *            provider is in the normal state
+     * @return true if the provider status is normal
+     */
+    private boolean checkProviderStatus(boolean loadData) {
+        View importFailureView = findViewById(R.id.import_failure);
+        if (importFailureView == null) {
+            return true;
+        }
+
+        // This query can be performed on the UI thread because
+        // the API explicitly allows such use.
+        Cursor cursor = getActivity().getContentResolver().query(ProviderStatus.CONTENT_URI,
+                new String[] { ProviderStatus.STATUS, ProviderStatus.DATA1 }, null, null, null);
+        if (cursor != null) {
+            try {
+                if (cursor.moveToFirst()) {
+                    int status = cursor.getInt(0);
+                    if (status != mProviderStatus) {
+                        mProviderStatus = status;
+                        switch (status) {
+                            case ProviderStatus.STATUS_NORMAL:
+                                mAdapter.notifyDataSetInvalidated();
+                                if (loadData) {
+                                    mLoader.forceLoad();
+                                }
+                                break;
+
+                            case ProviderStatus.STATUS_CHANGING_LOCALE:
+                                setEmptyText(R.string.locale_change_in_progress);
+                                mAdapter.changeCursor(null);
+                                mAdapter.notifyDataSetInvalidated();
+                                break;
+
+                            case ProviderStatus.STATUS_UPGRADING:
+                                setEmptyText(R.string.upgrade_in_progress);
+                                mAdapter.changeCursor(null);
+                                mAdapter.notifyDataSetInvalidated();
+                                break;
+
+                            case ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY:
+                                long size = cursor.getLong(1);
+                                String message = getActivity().getResources().getString(
+                                        R.string.upgrade_out_of_memory, new Object[] {size});
+                                TextView messageView = (TextView) findViewById(R.id.emptyText);
+                                messageView.setText(message);
+                                messageView.setVisibility(View.VISIBLE);
+                                configureImportFailureView(importFailureView);
+                                mAdapter.changeCursor(null);
+                                mAdapter.notifyDataSetInvalidated();
+                                break;
+                        }
+                    }
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+
+        importFailureView.setVisibility(
+                mProviderStatus == ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY
+                        ? View.VISIBLE
+                        : View.GONE);
+        return mProviderStatus == ProviderStatus.STATUS_NORMAL;
+    }
+
+    private void configureImportFailureView(View importFailureView) {
+
+        OnClickListener listener = new OnClickListener(){
+
+            public void onClick(View v) {
+                switch(v.getId()) {
+                    case R.id.import_failure_uninstall_apps: {
+                        // TODO break into a separate method
+                        getActivity().startActivity(
+                                new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS));
+                        break;
+                    }
+                    case R.id.import_failure_retry_upgrade: {
+                        // Send a provider status update, which will trigger a retry
+                        ContentValues values = new ContentValues();
+                        values.put(ProviderStatus.STATUS, ProviderStatus.STATUS_UPGRADING);
+                        getActivity().getContentResolver().update(ProviderStatus.CONTENT_URI,
+                                values, null, null);
+                        break;
+                    }
+                }
+            }};
+
+        Button uninstallApps = (Button) findViewById(R.id.import_failure_uninstall_apps);
+        uninstallApps.setOnClickListener(listener);
+
+        Button retryUpgrade = (Button) findViewById(R.id.import_failure_retry_upgrade);
+        retryUpgrade.setOnClickListener(listener);
+    }
+
+    private View findViewById(int id) {
+        return mView.findViewById(id);
+    }
+
+    // TODO: fix PluralRules to handle zero correctly and use Resources.getQuantityText directly
+    public String getQuantityText(int count, int zeroResourceId, int pluralResourceId) {
+        if (count == 0) {
+            return getActivity().getString(zeroResourceId);
+        } else {
+            String format = getActivity().getResources()
+                    .getQuantityText(pluralResourceId, count).toString();
+            return String.format(format, count);
+        }
+    }
+
+    protected void setEmptyText(int resourceId) {
+        TextView empty = (TextView) getEmptyView().findViewById(R.id.emptyText);
+        empty.setText(getActivity().getText(resourceId));
+        empty.setVisibility(View.VISIBLE);
+    }
+
+    // TODO redesign into an async task or loader
+    protected boolean isSyncActive() {
+        Account[] accounts = AccountManager.get(getActivity()).getAccounts();
+        if (accounts != null && accounts.length > 0) {
+            IContentService contentService = ContentResolver.getContentService();
+            for (Account account : accounts) {
+                try {
+                    if (contentService.isSyncActive(account, ContactsContract.AUTHORITY)) {
+                        return true;
+                    }
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Could not get the sync status");
+                }
+            }
+        }
+        return false;
+    }
+
+    protected boolean hasIccCard() {
+        TelephonyManager telephonyManager =
+                (TelephonyManager)getActivity().getSystemService(Context.TELEPHONY_SERVICE);
+        return telephonyManager.hasIccCard();
+    }
+
+    // TODO integrate into picker fragments
+//    protected Uri buildCallingPackageUri(Uri uri) {
+//        String callingPackage = getContext().getCallingPackage();
+//        if (!TextUtils.isEmpty(callingPackage)) {
+//            uri = uri.buildUpon().appendQueryParameter(
+//                    ContactsContract.REQUESTING_PACKAGE_PARAM_KEY, callingPackage).build();
+//        }
+//    }
+}
diff --git a/src/com/android/contacts/list/ContactListAdapter.java b/src/com/android/contacts/list/ContactListAdapter.java
new file mode 100644
index 0000000..3dfde7d
--- /dev/null
+++ b/src/com/android/contacts/list/ContactListAdapter.java
@@ -0,0 +1,207 @@
+/*
+ * 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.list;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.ContactCounts;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.SearchSnippetColumns;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+import android.widget.QuickContactBadge;
+
+/**
+ * A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type.
+ */
+public abstract class ContactListAdapter extends ContactEntryListAdapter {
+
+    protected static final String[] PROJECTION = new String[] {
+        Contacts._ID,                           // 0
+        Contacts.DISPLAY_NAME_PRIMARY,          // 1
+        Contacts.DISPLAY_NAME_ALTERNATIVE,      // 2
+        Contacts.SORT_KEY_PRIMARY,              // 3
+        Contacts.STARRED,                       // 4
+        Contacts.CONTACT_PRESENCE,              // 5
+        Contacts.PHOTO_ID,                      // 6
+        Contacts.LOOKUP_KEY,                    // 7
+        Contacts.PHONETIC_NAME,                 // 8
+        Contacts.HAS_PHONE_NUMBER,              // 9
+    };
+
+    protected static final String[] FILTER_PROJECTION = new String[] {
+        Contacts._ID,                           // 0
+        Contacts.DISPLAY_NAME_PRIMARY,          // 1
+        Contacts.DISPLAY_NAME_ALTERNATIVE,      // 2
+        Contacts.SORT_KEY_PRIMARY,              // 3
+        Contacts.STARRED,                       // 4
+        Contacts.CONTACT_PRESENCE,              // 5
+        Contacts.PHOTO_ID,                      // 6
+        Contacts.LOOKUP_KEY,                    // 7
+        Contacts.PHONETIC_NAME,                 // 8
+        Contacts.HAS_PHONE_NUMBER,              // 9
+        SearchSnippetColumns.SNIPPET_MIMETYPE,  // 10
+        SearchSnippetColumns.SNIPPET_DATA1,     // 11
+        SearchSnippetColumns.SNIPPET_DATA4,     // 12
+    };
+
+    protected static final int CONTACT_ID_COLUMN_INDEX = 0;
+    protected static final int CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX = 1;
+    protected static final int CONTACT_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX = 2;
+    protected static final int CONTACT_SORT_KEY_PRIMARY_COLUMN_INDEX = 3;
+    protected static final int CONTACT_STARRED_COLUMN_INDEX = 4;
+    protected static final int CONTACT_PRESENCE_STATUS_COLUMN_INDEX = 5;
+    protected static final int CONTACT_PHOTO_ID_COLUMN_INDEX = 6;
+    protected static final int CONTACT_LOOKUP_KEY_COLUMN_INDEX = 7;
+    protected static final int CONTACT_PHONETIC_NAME_COLUMN_INDEX = 8;
+    protected static final int CONTACT_HAS_PHONE_COLUMN_INDEX = 9;
+    protected static final int CONTACT_SNIPPET_MIMETYPE_COLUMN_INDEX = 10;
+    protected static final int CONTACT_SNIPPET_DATA1_COLUMN_INDEX = 11;
+    protected static final int CONTACT_SNIPPET_DATA4_COLUMN_INDEX = 12;
+
+    private boolean mQuickContactEnabled;
+    private CharSequence mUnknownNameText;
+    private int mDisplayNameColumnIndex;
+    private int mAlternativeDisplayNameColumnIndex;
+
+    public ContactListAdapter(Context context) {
+        super(context);
+
+        mUnknownNameText = context.getText(android.R.string.unknownName);
+    }
+
+    public CharSequence getUnknownNameText() {
+        return mUnknownNameText;
+    }
+
+    protected static Uri buildSectionIndexerUri(Uri uri) {
+        return uri.buildUpon()
+                .appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true").build();
+    }
+
+    public boolean getHasPhoneNumber() {
+        return getCursor().getInt(CONTACT_HAS_PHONE_COLUMN_INDEX) != 0;
+    }
+
+    public boolean isContactStarred() {
+        return getCursor().getInt(CONTACT_STARRED_COLUMN_INDEX) != 0;
+    }
+
+    @Override
+    public String getContactDisplayName() {
+        return getCursor().getString(mDisplayNameColumnIndex);
+    }
+
+    public boolean isQuickContactEnabled() {
+        return mQuickContactEnabled;
+    }
+
+    public void setQuickContactEnabled(boolean quickContactEnabled) {
+        mQuickContactEnabled = quickContactEnabled;
+    }
+
+    @Override
+    public void setContactNameDisplayOrder(int displayOrder) {
+        super.setContactNameDisplayOrder(displayOrder);
+        if (getContactNameDisplayOrder() == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
+            mDisplayNameColumnIndex = CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX;
+            mAlternativeDisplayNameColumnIndex = CONTACT_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX;
+        } else {
+            mDisplayNameColumnIndex = CONTACT_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX;
+            mAlternativeDisplayNameColumnIndex = CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX;
+        }
+    }
+
+    /**
+     * Builds the {@link Contacts#CONTENT_LOOKUP_URI} for the given
+     * {@link ListView} position.
+     */
+    public Uri getContactUri() {
+        Cursor cursor = getCursor();
+        long contactId = cursor.getLong(CONTACT_ID_COLUMN_INDEX);
+        String lookupKey = cursor.getString(CONTACT_LOOKUP_KEY_COLUMN_INDEX);
+        return Contacts.getLookupUri(contactId, lookupKey);
+    }
+
+    @Override
+    public View newView(Context context, Cursor cursor, ViewGroup parent) {
+        final ContactListItemView view = new ContactListItemView(context, null);
+        view.setUnknownNameText(mUnknownNameText);
+        view.setTextWithHighlightingFactory(getTextWithHighlightingFactory());
+        return view;
+    }
+
+    protected void bindSectionHeaderAndDivider(ContactListItemView view, Cursor cursor) {
+        bindSectionHeaderAndDivider(view, cursor.getPosition());
+    }
+
+    protected void bindSectionHeaderAndDivider(ContactListItemView view, int position) {
+        final int section = getSectionForPosition(position);
+        if (section != -1 && getPositionForSection(section) == position) {
+            String title = (String)getSections()[section];
+            view.setSectionHeader(title);
+        } else {
+            view.setDividerVisible(false);
+            view.setSectionHeader(null);
+        }
+
+        // move the divider for the last item in a section
+        if (getPositionForSection(section + 1) - 1 == position) {
+            view.setDividerVisible(false);
+        } else {
+            view.setDividerVisible(true);
+        }
+    }
+
+    protected void bindPhoto(final ContactListItemView view, Cursor cursor) {
+        // Set the photo, if available
+        long photoId = 0;
+        if (!cursor.isNull(CONTACT_PHOTO_ID_COLUMN_INDEX)) {
+            photoId = cursor.getLong(CONTACT_PHOTO_ID_COLUMN_INDEX);
+        }
+
+        getPhotoLoader().loadPhoto(view.getPhotoView(), photoId);
+    }
+
+    protected void bindQuickContact(final ContactListItemView view, Cursor cursor) {
+        long photoId = 0;
+        if (!cursor.isNull(CONTACT_PHOTO_ID_COLUMN_INDEX)) {
+            photoId = cursor.getLong(CONTACT_PHOTO_ID_COLUMN_INDEX);
+        }
+
+        QuickContactBadge quickContact = view.getQuickContact();
+        quickContact.assignContactUri(getContactUri());
+        getPhotoLoader().loadPhoto(quickContact, photoId);
+    }
+
+    protected void bindName(final ContactListItemView view, Cursor cursor) {
+        view.showDisplayName(cursor, mDisplayNameColumnIndex, isNameHighlightingEnabled(),
+                mAlternativeDisplayNameColumnIndex);
+        view.showPhoneticName(cursor, CONTACT_PHONETIC_NAME_COLUMN_INDEX);
+    }
+
+    protected void bindPresence(final ContactListItemView view, Cursor cursor) {
+        view.showPresence(cursor, CONTACT_PRESENCE_STATUS_COLUMN_INDEX);
+    }
+
+    protected void bindSearchSnippet(final ContactListItemView view, Cursor cursor) {
+        view.showSnippet(cursor, CONTACT_SNIPPET_MIMETYPE_COLUMN_INDEX,
+                CONTACT_SNIPPET_DATA1_COLUMN_INDEX, CONTACT_SNIPPET_DATA4_COLUMN_INDEX);
+    }
+}
diff --git a/src/com/android/contacts/ContactListItemView.java b/src/com/android/contacts/list/ContactListItemView.java
similarity index 67%
rename from src/com/android/contacts/ContactListItemView.java
rename to src/com/android/contacts/list/ContactListItemView.java
index 89e4265..28444a4 100644
--- a/src/com/android/contacts/ContactListItemView.java
+++ b/src/com/android/contacts/list/ContactListItemView.java
@@ -14,17 +14,25 @@
  * limitations under the License.
  */
 
-package com.android.contacts;
+package com.android.contacts.list;
 
-import com.android.contacts.ui.widget.DontPressWithParentImageView;
+import com.android.contacts.ContactPresenceIconUtil;
+import com.android.contacts.R;
+import com.android.contacts.widget.TextWithHighlighting;
+import com.android.contacts.widget.TextWithHighlightingFactory;
 
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
+import android.database.CharArrayBuffer;
+import android.database.Cursor;
 import android.graphics.Canvas;
 import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
 import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
 import android.text.TextUtils;
 import android.text.TextUtils.TruncateAt;
 import android.util.AttributeSet;
@@ -44,7 +52,7 @@
     private static final int QUICK_CONTACT_BADGE_STYLE =
             com.android.internal.R.attr.quickContactBadgeStyleWindowMedium;
 
-    private final Context mContext;
+    protected final Context mContext;
 
     private final int mPreferredHeight;
     private final int mVerticalDividerMargin;
@@ -58,7 +66,7 @@
     private final int mPresenceIconMargin;
     private final int mHeaderTextWidth;
 
-    private boolean mHorizontalDividerVisible;
+    private boolean mHorizontalDividerVisible = true;
     private Drawable mHorizontalDividerDrawable;
     private int mHorizontalDividerHeight;
 
@@ -74,6 +82,7 @@
     private QuickContactBadge mQuickContact;
     private ImageView mPhotoView;
     private TextView mNameTextView;
+    private TextView mPhoneticNameTextView;
     private DontPressWithParentImageView mCallButton;
     private TextView mLabelView;
     private TextView mDataView;
@@ -85,8 +94,38 @@
     private int mLine1Height;
     private int mLine2Height;
     private int mLine3Height;
+    private int mLine4Height;
 
     private OnClickListener mCallButtonClickListener;
+    private TextWithHighlightingFactory mTextWithHighlightingFactory;
+    public CharArrayBuffer nameBuffer = new CharArrayBuffer(128);
+    public CharArrayBuffer dataBuffer = new CharArrayBuffer(128);
+    public CharArrayBuffer highlightedTextBuffer = new CharArrayBuffer(128);
+    public TextWithHighlighting textWithHighlighting;
+    public CharArrayBuffer phoneticNameBuffer = new CharArrayBuffer(128);
+
+    private CharSequence mUnknownNameText;
+
+    /**
+     * Special class to allow the parent to be pressed without being pressed itself.
+     * This way the line of a tab can be pressed, but the image itself is not.
+     */
+    // TODO: understand this
+    private static class DontPressWithParentImageView extends ImageView {
+
+        public DontPressWithParentImageView(Context context, AttributeSet attrs) {
+            super(context, attrs);
+        }
+
+        @Override
+        public void setPressed(boolean pressed) {
+            // If the parent is pressed, do not set to pressed.
+            if (pressed && ((View) getParent()).isPressed()) {
+                return;
+            }
+            super.setPressed(pressed);
+        }
+    }
 
     public ContactListItemView(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -128,6 +167,14 @@
         mCallButtonClickListener = callButtonClickListener;
     }
 
+    public void setTextWithHighlightingFactory(TextWithHighlightingFactory factory) {
+        mTextWithHighlightingFactory = factory;
+    }
+
+    public void setUnknownNameText(CharSequence unknownNameText) {
+        mUnknownNameText = unknownNameText;
+    }
+
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         // We will match parent's width and wrap content vertically, but make sure
@@ -138,28 +185,36 @@
         mLine1Height = 0;
         mLine2Height = 0;
         mLine3Height = 0;
+        mLine4Height = 0;
 
         // Obtain the natural dimensions of the name text (we only care about height)
-        mNameTextView.measure(0, 0);
+        if (isVisible(mNameTextView)) {
+            mNameTextView.measure(0, 0);
+            mLine1Height = mNameTextView.getMeasuredHeight();
+        }
 
-        mLine1Height = mNameTextView.getMeasuredHeight();
+        if (isVisible(mPhoneticNameTextView)) {
+            mPhoneticNameTextView.measure(0, 0);
+            mLine2Height = mPhoneticNameTextView.getMeasuredHeight();
+        }
 
         if (isVisible(mLabelView)) {
             mLabelView.measure(0, 0);
-            mLine2Height = mLabelView.getMeasuredHeight();
+            mLine3Height = mLabelView.getMeasuredHeight();
         }
 
         if (isVisible(mDataView)) {
             mDataView.measure(0, 0);
-            mLine2Height = Math.max(mLine2Height, mDataView.getMeasuredHeight());
+            mLine3Height = Math.max(mLine3Height, mDataView.getMeasuredHeight());
         }
 
         if (isVisible(mSnippetView)) {
             mSnippetView.measure(0, 0);
-            mLine3Height = mSnippetView.getMeasuredHeight();
+            mLine4Height = mSnippetView.getMeasuredHeight();
         }
 
-        height += mLine1Height + mLine2Height + mLine3Height;
+        height += mLine1Height + mLine2Height + mLine3Height + mLine4Height
+                + mPaddingTop + mPaddingBottom;
 
         if (isVisible(mCallButton)) {
             mCallButton.measure(0, 0);
@@ -178,7 +233,12 @@
             mHeaderTextView.measure(
                     MeasureSpec.makeMeasureSpec(mHeaderTextWidth, MeasureSpec.EXACTLY),
                     MeasureSpec.makeMeasureSpec(mHeaderBackgroundHeight, MeasureSpec.EXACTLY));
-            height += mHeaderBackgroundDrawable.getIntrinsicHeight();
+            height += mHeaderBackgroundHeight;
+        }
+
+        if (mHorizontalDividerVisible) {
+            ensureHorizontalDivider();
+            height += mHorizontalDividerHeight;
         }
 
         setMeasuredDimension(width, height);
@@ -207,8 +267,73 @@
         // 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;
+        int leftBound = layoutLeftSide(height, topBound, mPaddingLeft);
+        int rightBound = layoutRightSide(height, topBound, right);
+
+        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 + mLine4Height;
+        int textTopBound = (bottomBound + topBound - totalTextHeight) / 2;
+
+        if (isVisible(mNameTextView)) {
+            mNameTextView.layout(leftBound,
+                    textTopBound,
+                    rightBound,
+                    textTopBound + mLine1Height);
+        }
+
+        int dataLeftBound = leftBound;
+        if (isVisible(mPhoneticNameTextView)) {
+            mPhoneticNameTextView.layout(leftBound,
+                    textTopBound + mLine1Height,
+                    rightBound,
+                    textTopBound + mLine1Height + mLine2Height);
+        }
+
+        if (isVisible(mLabelView)) {
+            dataLeftBound = leftBound + mLabelView.getMeasuredWidth();
+            mLabelView.layout(leftBound,
+                    textTopBound + mLine1Height + mLine2Height,
+                    dataLeftBound,
+                    textTopBound + mLine1Height + mLine2Height + mLine3Height);
+            dataLeftBound += mGapBetweenLabelAndData;
+        }
+
+        if (isVisible(mDataView)) {
+            mDataView.layout(dataLeftBound,
+                    textTopBound + mLine1Height + mLine2Height,
+                    rightBound,
+                    textTopBound + mLine1Height + mLine2Height + mLine3Height);
+        }
+
+        if (isVisible(mSnippetView)) {
+            mSnippetView.layout(leftBound,
+                    textTopBound + mLine1Height + mLine2Height + mLine3Height,
+                    rightBound,
+                    textTopBound + mLine1Height + mLine2Height + mLine3Height + mLine4Height);
+        }
+    }
+
+    /**
+     * Performs layout of the left side of the view
+     *
+     * @return new left boundary
+     */
+    protected int layoutLeftSide(int height, int topBound, int leftBound) {
         View photoView = mQuickContact != null ? mQuickContact : mPhotoView;
         if (photoView != null) {
             // Center the photo vertically
@@ -220,9 +345,15 @@
                     photoTop + mPhotoViewHeight);
             leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
         }
+        return leftBound;
+    }
 
-        // Right side
-        int rightBound = right;
+    /**
+     * Performs layout of the right side of the view
+     *
+     * @return new right boundary
+     */
+    protected int layoutRightSide(int height, int topBound, int rightBound) {
         if (isVisible(mCallButton)) {
             int buttonWidth = mCallButton.getMeasuredWidth();
             rightBound -= buttonWidth;
@@ -230,7 +361,7 @@
                     rightBound,
                     topBound,
                     rightBound + buttonWidth,
-                    height);
+                    height - mHorizontalDividerHeight);
             mVerticalDividerVisible = true;
             ensureVerticalDivider();
             rightBound -= mVerticalDividerWidth;
@@ -252,57 +383,10 @@
                     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);
-        }
+        return rightBound;
     }
 
-    private boolean isVisible(View view) {
+    protected boolean isVisible(View view) {
         return view != null && view.getVisibility() == View.VISIBLE;
     }
 
@@ -430,6 +514,24 @@
     }
 
     /**
+     * Removes the photo view.  Should not be needed once we start handling different
+     * types of views as different types of views from the List's perspective.
+     *
+     * @deprecated
+     */
+    @Deprecated
+    public void removePhotoView() {
+        if (mPhotoView != null) {
+            removeView(mPhotoView);
+            mPhotoView = null;
+        }
+        if (mQuickContact != null) {
+            removeView(mQuickContact);
+            mQuickContact = null;
+        }
+    }
+
+    /**
      * Returns the text view for the contact name, creating it if necessary.
      */
     public TextView getNameTextView() {
@@ -470,6 +572,36 @@
     }
 
     /**
+     * Adds or updates a text view for the phonetic name.
+     */
+    public void setPhoneticName(char[] text, int size) {
+        if (text == null || size == 0) {
+            if (mPhoneticNameTextView != null) {
+                mPhoneticNameTextView.setVisibility(View.GONE);
+            }
+        } else {
+            getPhoneticNameTextView();
+            mPhoneticNameTextView.setText(text, 0, size);
+            mPhoneticNameTextView.setVisibility(VISIBLE);
+        }
+    }
+
+    /**
+     * Returns the text view for the phonetic name, creating it if necessary.
+     */
+    public TextView getPhoneticNameTextView() {
+        if (mPhoneticNameTextView == null) {
+            mPhoneticNameTextView = new TextView(mContext);
+            mPhoneticNameTextView.setSingleLine(true);
+            mPhoneticNameTextView.setEllipsize(TruncateAt.MARQUEE);
+            mPhoneticNameTextView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
+            mPhoneticNameTextView.setTypeface(mPhoneticNameTextView.getTypeface(), Typeface.BOLD);
+            addView(mPhoneticNameTextView);
+        }
+        return mPhoneticNameTextView;
+    }
+
+    /**
      * Adds or updates a text view for the data label.
      */
     public void setLabel(CharSequence text) {
@@ -592,4 +724,98 @@
             }
         }
     }
+
+    public void showDisplayName(Cursor cursor, int nameColumnIndex, boolean highlightingEnabled,
+            int alternativeNameColumnIndex) {
+        cursor.copyStringToBuffer(nameColumnIndex, nameBuffer);
+        TextView nameView = getNameTextView();
+        int size = nameBuffer.sizeCopied;
+        if (size != 0) {
+            if (highlightingEnabled) {
+                if (textWithHighlighting == null) {
+                    textWithHighlighting =
+                            mTextWithHighlightingFactory.createTextWithHighlighting();
+                }
+                cursor.copyStringToBuffer(alternativeNameColumnIndex, highlightedTextBuffer);
+                textWithHighlighting.setText(nameBuffer, highlightedTextBuffer);
+                nameView.setText(textWithHighlighting);
+            } else {
+                nameView.setText(nameBuffer.data, 0, size);
+            }
+        } else {
+            nameView.setText(mUnknownNameText);
+        }
+    }
+
+    public void showPhoneticName(Cursor cursor, int phoneticNameColumnIndex) {
+        cursor.copyStringToBuffer(phoneticNameColumnIndex, phoneticNameBuffer);
+        int phoneticNameSize = phoneticNameBuffer.sizeCopied;
+        if (phoneticNameSize != 0) {
+            setPhoneticName(phoneticNameBuffer.data, phoneticNameSize);
+        } else {
+            setPhoneticName(null, 0);
+        }
+    }
+
+    /**
+     * Sets the proper icon (star or presence or nothing)
+     */
+    public void showPresence(Cursor cursor, int presenceColumnIndex) {
+        int serverStatus;
+        if (!cursor.isNull(presenceColumnIndex)) {
+            serverStatus = cursor.getInt(presenceColumnIndex);
+
+            // TODO consider caching these drawables
+            Drawable icon = ContactPresenceIconUtil.getPresenceIcon(getContext(), serverStatus);
+            if (icon != null) {
+                setPresence(icon);
+            } else {
+                setPresence(null);
+            }
+        } else {
+            setPresence(null);
+        }
+    }
+
+    /**
+     * Shows search snippet.
+     */
+    public void showSnippet(Cursor cursor, int summarySnippetMimetypeColumnIndex,
+            int summarySnippetData1ColumnIndex, int summarySnippetData4ColumnIndex) {
+        String snippet = null;
+        String snippetMimeType = cursor.getString(summarySnippetMimetypeColumnIndex);
+        if (Email.CONTENT_ITEM_TYPE.equals(snippetMimeType)) {
+            String email = cursor.getString(summarySnippetData1ColumnIndex);
+            if (!TextUtils.isEmpty(email)) {
+                snippet = email;
+            }
+        } else if (Organization.CONTENT_ITEM_TYPE.equals(snippetMimeType)) {
+            String company = cursor.getString(summarySnippetData1ColumnIndex);
+            String title = cursor.getString(summarySnippetData4ColumnIndex);
+            if (!TextUtils.isEmpty(company)) {
+                if (!TextUtils.isEmpty(title)) {
+                    snippet = company + " / " + title;
+                } else {
+                    snippet = company;
+                }
+            } else if (!TextUtils.isEmpty(title)) {
+                snippet = title;
+            }
+        } else if (Nickname.CONTENT_ITEM_TYPE.equals(snippetMimeType)) {
+            String nickname = cursor.getString(summarySnippetData1ColumnIndex);
+            if (!TextUtils.isEmpty(nickname)) {
+                snippet = nickname;
+            }
+        }
+
+        setSnippet(snippet);
+    }
+
+    /**
+     * Shows data element (e.g. phone number).
+     */
+    public void showData(Cursor cursor, int dataColumnIndex) {
+        cursor.copyStringToBuffer(dataColumnIndex, dataBuffer);
+        setData(dataBuffer.data, dataBuffer.sizeCopied);
+    }
 }
diff --git a/src/com/android/contacts/list/ContactPickerFragment.java b/src/com/android/contacts/list/ContactPickerFragment.java
new file mode 100644
index 0000000..d2b550a
--- /dev/null
+++ b/src/com/android/contacts/list/ContactPickerFragment.java
@@ -0,0 +1,169 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+import com.android.contacts.list.ShortcutIntentBuilder.OnShortcutIntentCreatedListener;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+
+/**
+ * Fragment for the contact list used for browsing contacts (as compared to
+ * picking a contact with one of the PICK or SHORTCUT intents).
+ */
+public class ContactPickerFragment extends ContactEntryListFragment<ContactEntryListAdapter>
+        implements OnShortcutIntentCreatedListener {
+
+    private OnContactPickerActionListener mListener;
+    private boolean mCreateContactEnabled;
+    private boolean mShortcutRequested;
+
+    public void setOnContactPickerActionListener(OnContactPickerActionListener listener) {
+        mListener = listener;
+    }
+
+    public boolean isCreateContactEnabled() {
+        return mCreateContactEnabled;
+    }
+
+    public void setCreateContactEnabled(boolean flag) {
+        this.mCreateContactEnabled = flag;
+    }
+
+    public boolean isShortcutRequested() {
+        return mShortcutRequested;
+    }
+
+    public void setShortcutRequested(boolean flag) {
+        mShortcutRequested = flag;
+    }
+
+    @Override
+    protected View createView(LayoutInflater inflater, ViewGroup container) {
+        View view = super.createView(inflater, container);
+        if (mCreateContactEnabled) {
+            getListView().addHeaderView(inflater.inflate(R.layout.create_new_contact, null, false));
+        }
+        return view;
+    }
+
+    public boolean isSearchAllContactsItemPosition(int position) {
+        return isSearchMode() && position == getAdapter().getCount() - 1;
+    }
+
+    @Override
+    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+        if (position == 0 && !isSearchMode() && mCreateContactEnabled) {
+            mListener.onCreateNewContactAction();
+        } else {
+            super.onItemClick(parent, view, position, id);
+        }
+    }
+
+    @Override
+    protected void onItemClick(int position, long id) {
+        if (isSearchAllContactsItemPosition(position)) {
+            mListener.onSearchAllContactsAction((String)null);
+        } else {
+            Uri uri;
+            ContactEntryListAdapter adapter = getAdapter();
+            adapter.moveToPosition(position);
+            if (isLegacyCompatibilityMode()) {
+                uri = ((LegacyContactListAdapter)adapter).getPersonUri();
+            } else {
+                uri = ((ContactListAdapter)adapter).getContactUri();
+            }
+            if (mShortcutRequested) {
+                ShortcutIntentBuilder builder = new ShortcutIntentBuilder(getActivity(), this);
+                builder.createContactShortcutIntent(uri);
+            } else {
+                mListener.onPickContactAction(uri);
+            }
+        }
+    }
+
+    @Override
+    protected ContactEntryListAdapter createListAdapter() {
+        if (!isLegacyCompatibilityMode()) {
+            ContactListAdapter adapter = new DefaultContactListAdapter(getActivity());
+            adapter.setSectionHeaderDisplayEnabled(true);
+            adapter.setDisplayPhotos(true);
+            adapter.setQuickContactEnabled(false);
+            return adapter;
+        } else {
+            LegacyContactListAdapter adapter = new LegacyContactListAdapter(getActivity());
+            adapter.setSectionHeaderDisplayEnabled(false);
+            adapter.setDisplayPhotos(false);
+            return adapter;
+        }
+    }
+
+    @Override
+    protected void configureAdapter() {
+        super.configureAdapter();
+        ContactEntryListAdapter adapter = getAdapter();
+
+        // If "Create new contact" is shown, don't display the empty list UI
+        adapter.setEmptyListEnabled(!isCreateContactEnabled());
+    }
+
+    @Override
+    protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+        if (isSearchMode()) {
+            return inflater.inflate(R.layout.contacts_search_content, null);
+        } else if (isSearchResultsMode()) {
+            return inflater.inflate(R.layout.contacts_list_search_results, null);
+        } else {
+            return inflater.inflate(R.layout.contacts_list_content, null);
+        }
+    }
+
+    @Override
+    protected void prepareEmptyView() {
+        if (isSearchMode()) {
+            return;
+        } else if (isSearchResultsMode()) {
+            setEmptyText(R.string.noMatchingContacts);
+        } else if (isSyncActive()) {
+            if (mShortcutRequested) {
+                // Help text is the same no matter whether there is SIM or not.
+                setEmptyText(R.string.noContactsHelpTextWithSyncForCreateShortcut);
+            } else if (hasIccCard()) {
+                setEmptyText(R.string.noContactsHelpTextWithSync);
+            } else {
+                setEmptyText(R.string.noContactsNoSimHelpTextWithSync);
+            }
+        } else {
+            if (mShortcutRequested) {
+                // Help text is the same no matter whether there is SIM or not.
+                setEmptyText(R.string.noContactsHelpTextWithSyncForCreateShortcut);
+            } else if (hasIccCard()) {
+                setEmptyText(R.string.noContactsHelpText);
+            } else {
+                setEmptyText(R.string.noContactsNoSimHelpText);
+            }
+        }
+    }
+
+    public void onShortcutIntentCreated(Uri uri, Intent shortcutIntent) {
+        mListener.onShortcutIntentCreated(shortcutIntent);
+    }
+}
diff --git a/src/com/android/contacts/list/ContactsIntentResolver.java b/src/com/android/contacts/list/ContactsIntentResolver.java
new file mode 100644
index 0000000..8388daa
--- /dev/null
+++ b/src/com/android/contacts/list/ContactsIntentResolver.java
@@ -0,0 +1,228 @@
+/*
+ * 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.list;
+
+import com.android.contacts.CallContactActivity;
+import com.android.contacts.ContactsSearchManager;
+import com.android.contacts.R;
+
+import android.app.Activity;
+import android.app.SearchManager;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Contacts.ContactMethods;
+import android.provider.Contacts.People;
+import android.provider.Contacts.Phones;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Intents;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.Intents.UI;
+import android.text.TextUtils;
+import android.util.Log;
+
+/**
+ * Parses a Contacts intent, extracting all relevant parts and packaging them
+ * as a {@link ContactsRequest} object.
+ */
+@SuppressWarnings("deprecation")
+public class ContactsIntentResolver {
+
+    private static final String TAG = "ContactsListActivity";
+
+    private final Activity mContext;
+
+    public ContactsIntentResolver(Activity context) {
+        this.mContext = context;
+    }
+
+    public ContactsRequest resolveIntent(Intent intent) {
+        ContactsRequest request = new ContactsRequest();
+        request.setDisplayOnlyVisible(true);
+
+        String action = intent.getAction();
+
+        Log.i(TAG, "Called with action: " + action);
+
+        if (UI.LIST_DEFAULT.equals(action) ) {
+            request.setActionCode(ContactsRequest.ACTION_DEFAULT);
+            request.setDisplayWithPhonesOnlyOption(
+                    ContactsRequest.DISPLAY_ONLY_WITH_PHONES_PREFERENCE);
+        } else if (UI.LIST_ALL_CONTACTS_ACTION.equals(action)) {
+            request.setActionCode(ContactsRequest.ACTION_DEFAULT);
+            request.setDisplayWithPhonesOnlyOption(
+                    ContactsRequest.DISPLAY_ONLY_WITH_PHONES_DISABLED);
+            request.setDisplayOnlyVisible(false);
+        } else if (UI.LIST_CONTACTS_WITH_PHONES_ACTION.equals(action)) {
+            request.setActionCode(ContactsRequest.ACTION_DEFAULT);
+            request.setDisplayWithPhonesOnlyOption(
+                    ContactsRequest.DISPLAY_ONLY_WITH_PHONES_ENABLED);
+        } else if (UI.LIST_STARRED_ACTION.equals(action)) {
+            request.setActionCode(ContactsRequest.ACTION_STARRED);
+        } else if (UI.LIST_FREQUENT_ACTION.equals(action)) {
+            request.setActionCode(ContactsRequest.ACTION_FREQUENT);
+        } else if (UI.LIST_STREQUENT_ACTION.equals(action)) {
+            request.setActionCode(ContactsRequest.ACTION_STREQUENT);
+        } else if (UI.LIST_GROUP_ACTION.equals(action)) {
+            request.setActionCode(ContactsRequest.ACTION_GROUP);
+            String groupName = intent.getStringExtra(UI.GROUP_NAME_EXTRA_KEY);
+            if (!TextUtils.isEmpty(groupName)) {
+                request.setGroupName(groupName);
+            } else {
+                Log.e(TAG, "Intent missing a required extra: " + UI.GROUP_NAME_EXTRA_KEY);
+                request.setValid(false);
+            }
+        } else if (Intent.ACTION_PICK.equals(action)) {
+            final String resolvedType = intent.resolveType(mContext);
+            if (Contacts.CONTENT_TYPE.equals(resolvedType)) {
+                request.setActionCode(ContactsRequest.ACTION_PICK_CONTACT);
+            } else if (People.CONTENT_TYPE.equals(resolvedType)) {
+                request.setActionCode(ContactsRequest.ACTION_PICK_CONTACT);
+                request.setLegacyCompatibilityMode(true);
+            } else if (Phone.CONTENT_TYPE.equals(resolvedType)) {
+                request.setActionCode(ContactsRequest.ACTION_PICK_PHONE);
+            } else if (Phones.CONTENT_TYPE.equals(resolvedType)) {
+                request.setActionCode(ContactsRequest.ACTION_PICK_PHONE);
+                request.setLegacyCompatibilityMode(true);
+            } else if (StructuredPostal.CONTENT_TYPE.equals(resolvedType)) {
+                request.setActionCode(ContactsRequest.ACTION_PICK_POSTAL);
+            } else if (ContactMethods.CONTENT_POSTAL_TYPE.equals(resolvedType)) {
+                request.setActionCode(ContactsRequest.ACTION_PICK_POSTAL);
+                request.setLegacyCompatibilityMode(true);
+            }
+        } else if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) {
+            String component = intent.getComponent().getClassName();
+            if (component.equals("alias.DialShortcut")) {
+                request.setActionCode(ContactsRequest.ACTION_CREATE_SHORTCUT_CALL);
+                request.setActivityTitle(mContext.getString(R.string.callShortcutActivityTitle));
+            } else if (component.equals("alias.MessageShortcut")) {
+                request.setActionCode(ContactsRequest.ACTION_CREATE_SHORTCUT_SMS);
+                request.setActivityTitle(mContext.getString(R.string.messageShortcutActivityTitle));
+            } else {
+                request.setActionCode(ContactsRequest.ACTION_CREATE_SHORTCUT_CONTACT);
+                request.setActivityTitle(mContext.getString(R.string.shortcutActivityTitle));
+            }
+        } else if (Intent.ACTION_GET_CONTENT.equals(action)) {
+            String type = intent.getType();
+            if (Contacts.CONTENT_ITEM_TYPE.equals(type)) {
+                request.setActionCode(ContactsRequest.ACTION_PICK_OR_CREATE_CONTACT);
+            } else if (Phone.CONTENT_ITEM_TYPE.equals(type)) {
+                request.setActionCode(ContactsRequest.ACTION_PICK_PHONE);
+            } else if (Phones.CONTENT_ITEM_TYPE.equals(type)) {
+                request.setActionCode(ContactsRequest.ACTION_PICK_PHONE);
+                request.setLegacyCompatibilityMode(true);
+            } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(type)) {
+                request.setActionCode(ContactsRequest.ACTION_PICK_POSTAL);
+            } else if (ContactMethods.CONTENT_POSTAL_ITEM_TYPE.equals(type)) {
+                request.setActionCode(ContactsRequest.ACTION_PICK_POSTAL);
+                request.setLegacyCompatibilityMode(true);
+            }  else if (People.CONTENT_ITEM_TYPE.equals(type)) {
+                request.setActionCode(ContactsRequest.ACTION_PICK_OR_CREATE_CONTACT);
+                request.setLegacyCompatibilityMode(true);
+            }
+        } else if (Intent.ACTION_INSERT_OR_EDIT.equals(action)) {
+            request.setActionCode(ContactsRequest.ACTION_INSERT_OR_EDIT_CONTACT);
+        } else if (Intent.ACTION_SEARCH.equals(action)) {
+            // See if the suggestion was clicked with a search action key (call button)
+            if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG))) {
+                String query = intent.getStringExtra(SearchManager.QUERY);
+                if (!TextUtils.isEmpty(query)) {
+                    Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
+                            Uri.fromParts("tel", query, null));
+                    request.setRedirectIntent(newIntent);
+                }
+            } else {
+                request.setQueryString(intent.getStringExtra(SearchManager.QUERY));
+                request.setSearchResultsMode(true);
+            }
+        } else if (UI.FILTER_CONTACTS_ACTION.equals(action)) {
+            // When we get a FILTER_CONTACTS_ACTION, it represents search in the context
+            // of some other action. Let's retrieve the original action to provide proper
+            // context for the search queries.
+            request.setActionCode(ContactsRequest.ACTION_DEFAULT);
+            Bundle extras = intent.getExtras();
+            if (extras != null) {
+                request.setQueryString(extras.getString(UI.FILTER_TEXT_EXTRA_KEY));
+
+                ContactsRequest originalRequest =
+                        (ContactsRequest)extras.get(ContactsSearchManager.ORIGINAL_REQUEST_KEY);
+                if (originalRequest != null) {
+                    request.copyFrom(originalRequest);
+                }
+            }
+
+            if (request == null) {
+                request = new ContactsRequest();
+            }
+
+            request.setSearchMode(true);
+//        } else if (ACTION_SEARCH_INTERNAL.equals(action)) {
+//            String originalAction = null;
+//            Bundle extras = intent.getExtras();
+//            if (extras != null) {
+//                originalAction = extras.getString(ContactsSearchManager.ORIGINAL_ACTION_EXTRA_KEY);
+//            }
+//            mShortcutAction = intent.getStringExtra(SHORTCUT_ACTION_KEY);
+//
+//            if (Intent.ACTION_INSERT_OR_EDIT.equals(originalAction)) {
+//                request.setActionCode(ContactsRequest.MODE_QUERY_PICK_TO_EDIT;
+//                mShowSearchSnippets = true;
+//                mQueryString = intent.getStringExtra(SearchManager.QUERY);
+//            } else if (mShortcutAction != null && intent.hasExtra(Insert.PHONE)) {
+//                request.setActionCode(ContactsRequest.MODE_QUERY_PICK_PHONE;
+//                mQueryMode = QUERY_MODE_TEL;
+//                mQueryString = intent.getStringExtra(Insert.PHONE);
+//            } else {
+//                request.setActionCode(ContactsRequest.MODE_QUERY_PICK;
+//                mQueryMode = QUERY_MODE_NONE;
+//                mShowSearchSnippets = true;
+//                mQueryString = intent.getStringExtra(SearchManager.QUERY);
+//            }
+//            mSearchResultsMode = true;
+        // Since this is the filter activity it receives all intents
+        // dispatched from the SearchManager for security reasons
+        // so we need to re-dispatch from here to the intended target.
+        } else if (Intents.SEARCH_SUGGESTION_CLICKED.equals(action)) {
+            Uri data = intent.getData();
+            // See if the suggestion was clicked with a search action key (call button)
+            if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG))) {
+                Intent newIntent = new Intent(mContext, CallContactActivity.class);
+                newIntent.setData(data);
+                request.setRedirectIntent(newIntent);
+            } else {
+                request.setRedirectIntent(new Intent(Intent.ACTION_VIEW, data));
+            }
+        } else if (Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED.equals(action)) {
+            request.setRedirectIntent(new Intent(Intent.ACTION_CALL_PRIVILEGED, intent.getData()));
+        } else if (Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED.equals(action)) {
+            // TODO actually support this in EditContactActivity.
+            String number = intent.getData().getSchemeSpecificPart();
+            Intent newIntent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
+            newIntent.putExtra(Intents.Insert.PHONE, number);
+            request.setRedirectIntent(newIntent);
+
+        }
+        // Allow the title to be set to a custom String using an extra on the intent
+        String title = intent.getStringExtra(UI.TITLE_EXTRA_KEY);
+        if (title != null) {
+            request.setActivityTitle(title);
+        }
+
+        return request;
+    }
+}
diff --git a/src/com/android/contacts/list/ContactsRequest.java b/src/com/android/contacts/list/ContactsRequest.java
new file mode 100644
index 0000000..6f2e2e4
--- /dev/null
+++ b/src/com/android/contacts/list/ContactsRequest.java
@@ -0,0 +1,229 @@
+/*
+ * 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.list;
+
+import android.content.Intent;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Parsed form of the intent sent to the Contacts application.
+ */
+public class ContactsRequest implements Parcelable {
+
+    /** Default mode: browse contacts */
+    public static final int ACTION_DEFAULT = 10;
+
+    /** Show contents of a specific group */
+    public static final int ACTION_GROUP = 20;
+
+    /** Show all starred contacts */
+    public static final int ACTION_STARRED = 30;
+
+    /** Show frequently contacted contacts */
+    public static final int ACTION_FREQUENT = 40;
+
+    /** Show starred and the frequent */
+    public static final int ACTION_STREQUENT = 50;
+
+    /** Show all contacts and pick them when clicking */
+    public static final int ACTION_PICK_CONTACT = 60;
+
+    /** Show all contacts as well as the option to create a new one */
+    public static final int ACTION_PICK_OR_CREATE_CONTACT = 70;
+
+    /** Show all contacts and pick them for edit when clicking, and allow creating a new contact */
+    public static final int ACTION_INSERT_OR_EDIT_CONTACT = 80;
+
+    /** Show all phone numbers and pick them when clicking */
+    public static final int ACTION_PICK_PHONE = 90;
+
+    /** Show all postal addresses and pick them when clicking */
+    public static final int ACTION_PICK_POSTAL = 100;
+
+    /** Show all contacts and create a shortcut for the picked contact */
+    public static final int ACTION_CREATE_SHORTCUT_CONTACT = 110;
+
+    /** Show all phone numbers and create a call shortcut for the picked number */
+    public static final int ACTION_CREATE_SHORTCUT_CALL = 120;
+
+    /** Show all phone numbers and create an SMS shortcut for the picked number */
+    public static final int ACTION_CREATE_SHORTCUT_SMS = 130;
+
+    private boolean mValid = true;
+    private int mActionCode = ACTION_DEFAULT;
+    private Intent mRedirectIntent;
+    private CharSequence mTitle;
+    private boolean mSearchMode;
+    private boolean mSearchResultsMode;
+    private String mQueryString;
+
+    public static final int DISPLAY_ONLY_WITH_PHONES_PREFERENCE = 0;
+    public static final int DISPLAY_ONLY_WITH_PHONES_ENABLED = 1;
+    public static final int DISPLAY_ONLY_WITH_PHONES_DISABLED = 2;
+
+    private int mDisplayOnlyWithPhones;
+    private boolean mDisplayOnlyVisible;
+    private String mGroupName;
+    private boolean mLegacyCompatibilityMode;
+
+    /**
+     * Copies all fields.
+     */
+    public void copyFrom(ContactsRequest request) {
+        mValid = request.mValid;
+        mActionCode = request.mActionCode;
+        mRedirectIntent = request.mRedirectIntent;
+        mTitle = request.mTitle;
+        mSearchMode = request.mSearchMode;
+        mSearchResultsMode = request.mSearchResultsMode;
+        mQueryString = request.mQueryString;
+        mDisplayOnlyWithPhones = request.mDisplayOnlyWithPhones;
+        mDisplayOnlyVisible = request.mDisplayOnlyVisible;
+        mGroupName = request.mGroupName;
+        mLegacyCompatibilityMode = request.mLegacyCompatibilityMode;
+    }
+
+    public static Parcelable.Creator<ContactsRequest> CREATOR = new Creator<ContactsRequest>() {
+
+        public ContactsRequest[] newArray(int size) {
+            return new ContactsRequest[size];
+        }
+
+        public ContactsRequest createFromParcel(Parcel source) {
+            ContactsRequest request = new ContactsRequest();
+            request.mValid = source.readInt() != 0;
+            request.mActionCode = source.readInt();
+            request.mRedirectIntent = source.readParcelable(this.getClass().getClassLoader());
+            request.mTitle = source.readCharSequence();
+            request.mSearchMode = source.readInt() != 0;
+            request.mSearchResultsMode = source.readInt() != 0;
+            request.mQueryString = source.readString();
+            request.mDisplayOnlyWithPhones = source.readInt();
+            request.mDisplayOnlyVisible = source.readInt() != 0;
+            request.mGroupName = source.readString();
+            request.mLegacyCompatibilityMode  = source.readInt() != 0;
+            return request;
+        }
+    };
+
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mValid ? 1 : 0);
+        dest.writeInt(mActionCode);
+        dest.writeParcelable(mRedirectIntent, 0);
+        dest.writeCharSequence(mTitle);
+        dest.writeInt(mSearchMode ? 1 : 0);
+        dest.writeInt(mSearchResultsMode ? 1 : 0);
+        dest.writeString(mQueryString);
+        dest.writeInt(mDisplayOnlyWithPhones);
+        dest.writeInt(mDisplayOnlyVisible ? 1 : 0);
+        dest.writeString(mGroupName);
+        dest.writeInt(mLegacyCompatibilityMode ? 1 : 0);
+    }
+
+    public int describeContents() {
+        return 0;
+    }
+
+    public boolean isValid() {
+        return mValid;
+    }
+
+    public void setValid(boolean flag) {
+        mValid = flag;
+    }
+
+    public Intent getRedirectIntent() {
+        return mRedirectIntent;
+    }
+
+    public void setRedirectIntent(Intent intent) {
+        mRedirectIntent = intent;
+    }
+
+    public void setActivityTitle(CharSequence title) {
+        mTitle = title;
+    }
+
+    public CharSequence getActivityTitle() {
+        return mTitle;
+    }
+
+    public int getActionCode() {
+        return mActionCode;
+    }
+
+    public void setActionCode(int actionCode) {
+        mActionCode = actionCode;
+    }
+
+    public boolean getDisplayOnlyVisible() {
+        return mDisplayOnlyVisible;
+    }
+
+    public void setDisplayOnlyVisible(boolean flag) {
+        mDisplayOnlyVisible = flag;
+    }
+
+    public int getDisplayWithPhonesOnlyOption() {
+        return mDisplayOnlyWithPhones;
+    }
+
+    public void setDisplayWithPhonesOnlyOption(int option) {
+        mDisplayOnlyWithPhones = option;
+    }
+
+    public boolean isSearchMode() {
+        return mSearchMode;
+    }
+
+    public void setSearchMode(boolean flag) {
+        mSearchMode = flag;
+    }
+
+    public boolean isSearchResultsMode() {
+        return mSearchResultsMode;
+    }
+
+    public void setSearchResultsMode(boolean flag) {
+        mSearchResultsMode = flag;
+    }
+
+    public String getQueryString() {
+        return mQueryString;
+    }
+
+    public void setQueryString(String string) {
+        mQueryString = string;
+    }
+
+    public String getGroupName() {
+        return mGroupName;
+    }
+
+    public void setGroupName(String groupName) {
+        mGroupName = groupName;
+    }
+
+    public boolean isLegacyCompatibilityMode() {
+        return mLegacyCompatibilityMode;
+    }
+
+    public void setLegacyCompatibilityMode(boolean flag) {
+        mLegacyCompatibilityMode = flag;
+    }
+}
diff --git a/src/com/android/contacts/list/DefaultContactBrowseListFragment.java b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
new file mode 100644
index 0000000..179d25a
--- /dev/null
+++ b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
@@ -0,0 +1,182 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+import com.android.contacts.ui.ContactsPreferencesActivity.Prefs;
+
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.preference.PreferenceManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+/**
+ * Fragment containing a contact list used for browsing (as compared to
+ * picking a contact with one of the PICK intents).
+ */
+public class DefaultContactBrowseListFragment extends ContactBrowseListFragment {
+
+    private boolean mEditMode;
+    private boolean mCreateContactEnabled;
+    private int mDisplayWithPhonesOnlyOption = ContactsRequest.DISPLAY_ONLY_WITH_PHONES_DISABLED;
+    private boolean mVisibleContactsRestrictionEnabled = true;
+
+    public DefaultContactBrowseListFragment() {
+        setPhotoLoaderEnabled(true);
+        setSectionHeaderDisplayEnabled(true);
+    }
+
+    @Override
+    protected void prepareEmptyView() {
+        if (isShowingContactsWithPhonesOnly()) {
+            setEmptyText(R.string.noContactsWithPhoneNumbers);
+        } else {
+            super.prepareEmptyView();
+        }
+    }
+
+    private boolean isShowingContactsWithPhonesOnly() {
+        switch (mDisplayWithPhonesOnlyOption) {
+            case ContactsRequest.DISPLAY_ONLY_WITH_PHONES_DISABLED:
+                return false;
+            case ContactsRequest.DISPLAY_ONLY_WITH_PHONES_ENABLED:
+                return true;
+            case ContactsRequest.DISPLAY_ONLY_WITH_PHONES_PREFERENCE:
+                SharedPreferences prefs = PreferenceManager
+                        .getDefaultSharedPreferences(getActivity());
+                return prefs.getBoolean(Prefs.DISPLAY_ONLY_PHONES,
+                        Prefs.DISPLAY_ONLY_PHONES_DEFAULT);
+        }
+        return false;
+    }
+
+    public void setDisplayWithPhonesOnlyOption(int displayWithPhonesOnly) {
+        mDisplayWithPhonesOnlyOption = displayWithPhonesOnly;
+        configureAdapter();
+    }
+
+    public void setVisibleContactsRestrictionEnabled(boolean flag) {
+        mVisibleContactsRestrictionEnabled = flag;
+        configureAdapter();
+    }
+
+    @Override
+    protected void onItemClick(int position, long id) {
+        ContactListAdapter adapter = getAdapter();
+        if (adapter.isSearchAllContactsItemPosition(position)) {
+            searchAllContacts();
+        } else {
+            if (isEditMode()) {
+                if (position == 0 && !isSearchMode() && isCreateContactEnabled()) {
+                    createNewContact();
+                } else {
+                    adapter.moveToPosition(position);
+                    editContact(adapter.getContactUri());
+                }
+            } else {
+                adapter.moveToPosition(position);
+                viewContact(adapter.getContactUri());
+            }
+        }
+    }
+
+    @Override
+    protected ContactListAdapter createListAdapter() {
+        DefaultContactListAdapter adapter = new DefaultContactListAdapter(getActivity());
+        adapter.setSectionHeaderDisplayEnabled(isSectionHeaderDisplayEnabled());
+        adapter.setDisplayPhotos(true);
+        adapter.setQuickContactEnabled(true);
+        return adapter;
+    }
+
+    @Override
+    protected void configureAdapter() {
+        super.configureAdapter();
+
+        DefaultContactListAdapter adapter = (DefaultContactListAdapter)getAdapter();
+        if (adapter != null) {
+            adapter.setContactsWithPhoneNumbersOnly(isShowingContactsWithPhonesOnly());
+            adapter.setVisibleContactsOnly(mVisibleContactsRestrictionEnabled);
+        }
+    }
+
+    @Override
+    protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+        if (isSearchResultsMode()) {
+            return inflater.inflate(R.layout.contacts_list_search_results, null);
+        } else {
+            return inflater.inflate(R.layout.contacts_list_content, null);
+        }
+    }
+
+    @Override
+    protected View createView(LayoutInflater inflater, ViewGroup container) {
+        View view = super.createView(inflater, container);
+        if (!isSearchResultsMode()) {
+            // In the search-results mode the count is shown in the fat header above the list
+            getListView().addHeaderView(inflater.inflate(R.layout.total_contacts, null, false));
+        }
+        return view;
+    }
+
+    @Override
+    protected void showCount(Cursor data) {
+        int count = data.getCount();
+        if (isSearchMode()) {
+            TextView textView = (TextView) getView().findViewById(R.id.totalContactsText);
+            // TODO
+            // if (TextUtils.isEmpty(getQueryString())) {
+            String text = getQuantityText(count, R.string.listFoundAllContactsZero,
+                    R.plurals.searchFoundContacts);
+            textView.setText(text);
+        }
+        else if (isSearchResultsMode()) {
+            TextView countText = (TextView)getView().findViewById(R.id.search_results_found);
+            String text = getQuantityText(data.getCount(),
+                    R.string.listFoundAllContactsZero, R.plurals.listFoundAllContacts);
+            countText.setText(text);
+        } else {
+            // TODO
+            // if (contactsListActivity.mDisplayOnlyPhones) {
+            // text = contactsListActivity.getQuantityText(count,
+            // R.string.listTotalPhoneContactsZero,
+            // R.plurals.listTotalPhoneContacts);
+            TextView textView = (TextView)getView().findViewById(R.id.totalContactsText);
+            String text = getQuantityText(count, R.string.listTotalAllContactsZero,
+                    R.plurals.listTotalAllContacts);
+            textView.setText(text);
+        }
+    }
+
+    public void setEditMode(boolean flag) {
+        mEditMode = flag;
+    }
+
+    public boolean isEditMode() {
+        return mEditMode;
+    }
+
+    public void setCreateContactEnabled(boolean flag) {
+        this.mCreateContactEnabled = flag;
+    }
+
+    public boolean isCreateContactEnabled() {
+        return mCreateContactEnabled;
+    }
+}
diff --git a/src/com/android/contacts/list/DefaultContactListAdapter.java b/src/com/android/contacts/list/DefaultContactListAdapter.java
new file mode 100644
index 0000000..4c4323f
--- /dev/null
+++ b/src/com/android/contacts/list/DefaultContactListAdapter.java
@@ -0,0 +1,102 @@
+/*
+ * 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.list;
+
+import android.app.patterns.CursorLoader;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.text.TextUtils;
+import android.view.View;
+
+/**
+ * A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type.
+ */
+public class DefaultContactListAdapter extends ContactListAdapter {
+
+    private boolean mContactsWithPhoneNumbersOnly;
+    private boolean mVisibleContactsOnly;
+
+    public DefaultContactListAdapter(Context context) {
+        super(context);
+    }
+
+    public void setContactsWithPhoneNumbersOnly(boolean flag) {
+        mContactsWithPhoneNumbersOnly = flag;
+    }
+
+    public void setVisibleContactsOnly(boolean flag) {
+        mVisibleContactsOnly = flag;
+    }
+
+    @Override
+    public void configureLoader(CursorLoader loader) {
+        Uri uri;
+
+        if (isSearchMode() || isSearchResultsMode()) {
+            String query = getQueryString();
+            uri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI,
+                    TextUtils.isEmpty(query) ? "" : Uri.encode(query));
+            loader.setProjection(FILTER_PROJECTION);
+        } else {
+            uri = Contacts.CONTENT_URI;
+            loader.setProjection(PROJECTION);
+        }
+
+        if (mVisibleContactsOnly && mContactsWithPhoneNumbersOnly) {
+            loader.setSelection(Contacts.IN_VISIBLE_GROUP + "=1"
+                    + " AND " + Contacts.HAS_PHONE_NUMBER + "=1");
+        } else if (mVisibleContactsOnly) {
+            loader.setSelection(Contacts.IN_VISIBLE_GROUP + "=1");
+        } else if (mContactsWithPhoneNumbersOnly) {
+            loader.setSelection(Contacts.HAS_PHONE_NUMBER + "=1");
+        }
+
+        if (isSectionHeaderDisplayEnabled()) {
+            uri = buildSectionIndexerUri(uri);
+        }
+
+        loader.setUri(uri);
+
+        if (getSortOrder() == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
+            loader.setSortOrder(Contacts.SORT_KEY_PRIMARY);
+        } else {
+            loader.setSortOrder(Contacts.SORT_KEY_ALTERNATIVE);
+        }
+    }
+
+    @Override
+    public void bindView(View itemView, Context context, Cursor cursor) {
+        final ContactListItemView view = (ContactListItemView)itemView;
+
+        bindSectionHeaderAndDivider(view, cursor);
+
+        if (isQuickContactEnabled()) {
+            bindQuickContact(view, cursor);
+        } else {
+            bindPhoto(view, cursor);
+        }
+
+        bindName(view, cursor);
+        bindPresence(view, cursor);
+
+        if (isSearchMode() || isSearchResultsMode()) {
+            bindSearchSnippet(view, cursor);
+        }
+    }
+}
diff --git a/src/com/android/contacts/list/JoinContactListAdapter.java b/src/com/android/contacts/list/JoinContactListAdapter.java
new file mode 100644
index 0000000..614e4e5
--- /dev/null
+++ b/src/com/android/contacts/list/JoinContactListAdapter.java
@@ -0,0 +1,314 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+
+import android.app.patterns.CursorLoader;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Contacts.AggregationSuggestions;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+public class JoinContactListAdapter extends ContactListAdapter {
+
+    /** Maximum number of suggestions shown for joining aggregates */
+    private static final int MAX_SUGGESTIONS = 4;
+
+    private Cursor mSuggestionsCursor;
+    private int mSuggestionsCursorCount;
+    private long mTargetContactId;
+
+    /**
+     * Determines whether we display a list item with the label
+     * "Show all contacts" or actually show all contacts
+     */
+    private boolean mAllContactsListShown;
+
+    public JoinContactListAdapter(Context context) {
+        super(context);
+        setSectionHeaderDisplayEnabled(true);
+    }
+
+    public void setTargetContactId(long targetContactId) {
+        this.mTargetContactId = targetContactId;
+    }
+
+    @Override
+    public void configureLoader(CursorLoader cursorLoader) {
+        JoinContactLoader loader = (JoinContactLoader)cursorLoader;
+        loader.setLoadSuggestionsAndAllContacts(mAllContactsListShown);
+
+        Builder builder = Contacts.CONTENT_URI.buildUpon();
+        builder.appendEncodedPath(String.valueOf(mTargetContactId));
+        builder.appendEncodedPath(AggregationSuggestions.CONTENT_DIRECTORY);
+
+        String filter = getQueryString();
+        if (!TextUtils.isEmpty(filter)) {
+            builder.appendEncodedPath(Uri.encode(filter));
+        }
+
+        builder.appendQueryParameter("limit", String.valueOf(MAX_SUGGESTIONS));
+
+        loader.setSuggestionUri(builder.build());
+
+        // TODO simplify projection
+        loader.setProjection(PROJECTION);
+
+        if (mAllContactsListShown) {
+            loader.setUri(buildSectionIndexerUri(Contacts.CONTENT_URI));
+            loader.setSelection(Contacts.IN_VISIBLE_GROUP + "=1 AND " + Contacts._ID + "!=?");
+            loader.setSelectionArgs(new String[]{String.valueOf(mTargetContactId)});
+            if (getSortOrder() == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
+                loader.setSortOrder(Contacts.SORT_KEY_PRIMARY);
+            } else {
+                loader.setSortOrder(Contacts.SORT_KEY_ALTERNATIVE);
+            }
+        }
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return false;
+    }
+
+    private boolean hasSuggestions() {
+        return mSuggestionsCursorCount != 0;
+    }
+
+    public boolean isAllContactsListShown() {
+        return mAllContactsListShown;
+    }
+
+    public void setAllContactsListShown(boolean flag) {
+        mAllContactsListShown = flag;
+    }
+
+    public void setSuggestionsCursor(Cursor cursor) {
+        mSuggestionsCursor = cursor;
+        mSuggestionsCursorCount = cursor == null ? 0 : cursor.getCount();
+    }
+
+    public boolean isShowAllContactsItemPosition(int position) {
+        return !mAllContactsListShown
+                && hasSuggestions() && position == mSuggestionsCursorCount + 2;
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        if (!mDataValid) {
+            throw new IllegalStateException(
+                    "this should only be called when the cursor is valid");
+        }
+
+        Cursor cursor;
+        boolean showingSuggestion = false;
+        if (hasSuggestions()) {
+            if (position == 0) {
+                // First section: "suggestions"
+                TextView view = (TextView) inflate(R.layout.list_separator, parent);
+                view.setText(R.string.separatorJoinAggregateSuggestions);
+                return view;
+            } else if (position < mSuggestionsCursorCount + 1) {
+                showingSuggestion = true;
+                cursor = mSuggestionsCursor;
+                cursor.moveToPosition(position - 1);
+            } else if (position == mSuggestionsCursorCount + 1) {
+                // Second section: "all contacts"
+                TextView view = (TextView) inflate(R.layout.list_separator, parent);
+                view.setText(R.string.separatorJoinAggregateAll);
+                return view;
+            } else if (!mAllContactsListShown && position == mSuggestionsCursorCount + 2) {
+                return inflate(R.layout.contacts_list_show_all_item, parent);
+            } else {
+                cursor = mCursor;
+                cursor.moveToPosition(position - mSuggestionsCursorCount - 2);
+            }
+        } else {
+            cursor = mCursor;
+            cursor.moveToPosition(position);
+        }
+
+        View v;
+        if (convertView == null || convertView.getTag() == null) {
+            v = newView(getContext(), cursor, parent);
+        } else {
+            v = convertView;
+        }
+        bindView(position, v, cursor, showingSuggestion);
+        return v;
+    }
+
+    private View inflate(int layoutId, ViewGroup parent) {
+        return LayoutInflater.from(getContext()).inflate(layoutId, parent, false);
+    }
+
+    @Override
+    public void bindView(View view, Context context, Cursor cursor) {
+        // not used
+    }
+
+    public void bindView(int position, View itemView, Cursor cursor, boolean showingSuggestion) {
+        final ContactListItemView view = (ContactListItemView)itemView;
+        if (!showingSuggestion) {
+            bindSectionHeaderAndDivider(view, position);
+        }
+        bindPhoto(view, cursor);
+        bindName(view, cursor);
+    }
+
+    public Cursor getShowAllContactsLabelCursor() {
+        MatrixCursor matrixCursor = new MatrixCursor(PROJECTION);
+        Object[] row = new Object[PROJECTION.length];
+        matrixCursor.addRow(row);
+        return matrixCursor;
+    }
+
+    @Override
+    public void changeCursor(Cursor cursor) {
+        if (cursor == null) {
+            setSuggestionsCursor(null);
+        }
+
+        super.changeCursor(cursor);
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        if (isShowAllContactsItemPosition(position)) {
+            return IGNORE_ITEM_VIEW_TYPE;
+        }
+
+        return super.getItemViewType(position);
+    }
+
+    @Override
+    public int getPositionForSection(int sectionIndex) {
+        if (mSuggestionsCursorCount == 0) {
+            return super.getPositionForSection(sectionIndex);
+        }
+
+        // Get section position in the full list
+        int position = super.getPositionForSection(sectionIndex);
+        return position + mSuggestionsCursorCount + 2;
+    }
+
+    @Override
+    public int getSectionForPosition(int position) {
+        if (mSuggestionsCursorCount == 0) {
+            return super.getSectionForPosition(position);
+        }
+
+        if (position < mSuggestionsCursorCount + 2) {
+            return -1;
+        }
+
+        return super.getSectionForPosition(position - mSuggestionsCursorCount - 2);
+    }
+
+    @Override
+    public boolean areAllItemsEnabled() {
+        return super.areAllItemsEnabled() && mSuggestionsCursorCount == 0;
+    }
+
+    @Override
+    public boolean isEnabled(int position) {
+        if (position == 0) {
+            return false;
+        }
+
+        if (mSuggestionsCursorCount > 0) {
+            return position != 0 && position != mSuggestionsCursorCount + 1;
+        }
+        return true;
+    }
+
+    @Override
+    public int getCount() {
+        if (!mDataValid) {
+            return 0;
+        }
+        int superCount = super.getCount();
+        if (hasSuggestions()) {
+            // When showing suggestions, we have 2 additional list items: the "Suggestions"
+            // and "All contacts" headers.
+            return mSuggestionsCursorCount + superCount + 2;
+        }
+        return superCount;
+    }
+
+    public int getSuggestionsCursorCount() {
+        return mSuggestionsCursorCount;
+    }
+
+    @Override
+    public Object getItem(int pos) {
+        if (hasSuggestions()) {
+            // When showing suggestions, we have 2 additional list items: the "Suggestions"
+            // and "All contacts" separators.
+            if (pos == 0) {
+                return null;
+            }
+            else if (pos < mSuggestionsCursorCount + 1) {
+                // We are in the upper partition (Suggestions). Adjusting for the "Suggestions"
+                // separator.
+                mSuggestionsCursor.moveToPosition(pos - 1);
+                return mSuggestionsCursor;
+            } else if (pos == mSuggestionsCursorCount + 1) {
+                // This is the "All contacts" separator
+                return null;
+            } else {
+                if (!isAllContactsListShown()) {
+                    // This is the "Show all contacts" item
+                    return null;
+                } else {
+                    // We are in the lower partition (All contacts). Adjusting for the size
+                    // of the upper partition plus the two separators.
+                    mCursor.moveToPosition(pos - mSuggestionsCursorCount - 2);
+                    return mCursor;
+                }
+            }
+        } else if (mCursor != null) {
+            // No separators
+            mCursor.moveToPosition(pos);
+            return mCursor;
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public long getItemId(int pos) {
+        Cursor cursor = (Cursor)getItem(pos);
+        return cursor == null ? 0 : cursor.getLong(mRowIDColumn);
+    }
+
+    public Uri getContactUri(int position) {
+        Cursor cursor = (Cursor)getItem(position);
+        long contactId = cursor.getLong(CONTACT_ID_COLUMN_INDEX);
+        String lookupKey = cursor.getString(CONTACT_LOOKUP_KEY_COLUMN_INDEX);
+        return Contacts.getLookupUri(contactId, lookupKey);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/list/JoinContactListFragment.java b/src/com/android/contacts/list/JoinContactListFragment.java
new file mode 100644
index 0000000..202ee67
--- /dev/null
+++ b/src/com/android/contacts/list/JoinContactListFragment.java
@@ -0,0 +1,128 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+
+import android.app.Activity;
+import android.app.patterns.CursorLoader;
+import android.app.patterns.Loader;
+import android.content.ContentUris;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.provider.ContactsContract.Contacts;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+/**
+ * Fragment for the Join Contact list.
+ */
+public class JoinContactListFragment extends ContactEntryListFragment<JoinContactListAdapter> {
+
+    private static final int DISPLAY_NAME_LOADER = 1;
+
+    private OnContactPickerActionListener mListener;
+    private long mTargetContactId;
+    private boolean mAllContactsListShown = false;
+
+    public JoinContactListFragment() {
+        setPhotoLoaderEnabled(true);
+        setSectionHeaderDisplayEnabled(true);
+    }
+
+    public void setOnContactPickerActionListener(OnContactPickerActionListener listener) {
+        mListener = listener;
+    }
+
+    @Override
+    protected void onInitializeLoaders() {
+        super.onInitializeLoaders();
+        startLoading(DISPLAY_NAME_LOADER, null);
+    }
+
+    @Override
+    protected Loader<Cursor> onCreateLoader(int id, Bundle args) {
+        if (id == DISPLAY_NAME_LOADER) {
+            // Loader for the display name of the target contact
+            return new CursorLoader(getActivity(),
+                    ContentUris.withAppendedId(Contacts.CONTENT_URI, mTargetContactId),
+                    new String[] {Contacts.DISPLAY_NAME}, null, null, null);
+        } else {
+            return new JoinContactLoader(getActivity());
+        }
+    }
+
+    @Override
+    protected void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+        if (loader.getId() == DISPLAY_NAME_LOADER) {
+            if (data != null && data.moveToFirst()) {
+                showTargetContactName(data.getString(0));
+            }
+        } else {
+            JoinContactListAdapter adapter = getAdapter();
+            if (adapter.isAllContactsListShown()) {
+                Cursor suggestionsCursor = ((JoinContactLoader)loader).getSuggestionsCursor();
+                adapter.setSuggestionsCursor(suggestionsCursor);
+                super.onLoadFinished(loader, data);
+            } else {
+                adapter.setSuggestionsCursor(data);
+                super.onLoadFinished(loader, adapter.getShowAllContactsLabelCursor());
+            }
+        }
+    }
+
+     private void showTargetContactName(String displayName) {
+        Activity activity = getActivity();
+        TextView blurbView = (TextView)activity.findViewById(R.id.join_contact_blurb);
+        String blurb = activity.getString(R.string.blurbJoinContactDataWith, displayName);
+        blurbView.setText(blurb);
+    }
+
+    public void setTargetContactId(long targetContactId) {
+        mTargetContactId = targetContactId;
+    }
+
+    @Override
+    public JoinContactListAdapter createListAdapter() {
+        return new JoinContactListAdapter(getActivity());
+    }
+
+    @Override
+    protected void configureAdapter() {
+        super.configureAdapter();
+        JoinContactListAdapter adapter = getAdapter();
+        adapter.setAllContactsListShown(mAllContactsListShown);
+        adapter.setTargetContactId(mTargetContactId);
+    }
+
+    @Override
+    protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+        return inflater.inflate(R.layout.contacts_list_content_join, null);
+    }
+
+    @Override
+    protected void onItemClick(int position, long id) {
+        JoinContactListAdapter adapter = getAdapter();
+        if (adapter.isShowAllContactsItemPosition(position)) {
+            mAllContactsListShown = true;
+            reloadData();
+        } else {
+            mListener.onPickContactAction(adapter.getContactUri(position));
+        }
+    }
+}
diff --git a/src/com/android/contacts/list/JoinContactLoader.java b/src/com/android/contacts/list/JoinContactLoader.java
new file mode 100644
index 0000000..c15acb1
--- /dev/null
+++ b/src/com/android/contacts/list/JoinContactLoader.java
@@ -0,0 +1,95 @@
+/*
+ * 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.list;
+
+import android.app.patterns.CursorLoader;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+
+/**
+ * A specialized loader for the Join Contacts UI.  It executes up to two queries:
+ * join suggestions and (optionally) the full contact list.
+ */
+public class JoinContactLoader extends CursorLoader {
+
+    private boolean mLoadSuggestionsAndAllContact;
+    private String[] mProjection;
+    private Uri mSuggestionUri;
+    private MatrixCursor mCachedCursor;
+
+    public JoinContactLoader(Context context) {
+        super(context, null, null, null, null, null);
+    }
+
+    public void setSuggestionUri(Uri uri) {
+        this.mSuggestionUri = uri;
+    }
+
+    @Override
+    public void setProjection(String[] projection) {
+        super.setProjection(projection);
+        this.mProjection = projection;
+    }
+
+    public Cursor getSuggestionsCursor() {
+        return mCachedCursor;
+    }
+
+    @Override
+    protected Cursor loadInBackground() {
+        if (mLoadSuggestionsAndAllContact) {
+            // First execute the suggestions query, then call super.loadInBackground
+            // to load the entire list
+            mCachedCursor = loadSuggestions();
+            return super.loadInBackground();
+        } else {
+            // Use the default behavior of the super.loadInBackground to load join
+            // suggestions only
+            setUri(mSuggestionUri);
+            setSelection(null);
+            setSelectionArgs(null);
+            setSortOrder(null);
+            return super.loadInBackground();
+        }
+    }
+
+    /**
+     * Loads join suggestions into a MatrixCursor.
+     */
+    private MatrixCursor loadSuggestions() {
+        Cursor cursor = getContext().getContentResolver().query(mSuggestionUri, mProjection,
+                null, null, null);
+        try {
+            MatrixCursor matrix = new MatrixCursor(mProjection);
+            Object[] row = new Object[mProjection.length];
+            while (cursor.moveToNext()) {
+                for (int i = 0; i < row.length; i++) {
+                    row[i] = cursor.getString(i);
+                }
+                matrix.addRow(row);
+            }
+            return matrix;
+        } finally {
+            cursor.close();
+        }
+    }
+
+    public void setLoadSuggestionsAndAllContacts(boolean flag) {
+        mLoadSuggestionsAndAllContact = flag;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/list/LegacyContactListAdapter.java b/src/com/android/contacts/list/LegacyContactListAdapter.java
new file mode 100644
index 0000000..753596b
--- /dev/null
+++ b/src/com/android/contacts/list/LegacyContactListAdapter.java
@@ -0,0 +1,100 @@
+/*
+ * 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.list;
+
+import android.app.patterns.CursorLoader;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.Contacts.People;
+import android.provider.ContactsContract.Contacts;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+
+/**
+ * A cursor adapter for the People.CONTENT_TYPE content type.
+ */
+@SuppressWarnings("deprecation")
+public class LegacyContactListAdapter extends ContactEntryListAdapter {
+
+    static final String[] PEOPLE_PROJECTION = new String[] {
+        People._ID,                         // 0
+        People.DISPLAY_NAME,                // 1
+        People.PHONETIC_NAME,               // 2
+        People.STARRED,                     // 3
+        People.PRESENCE_STATUS,             // 4
+    };
+
+    protected static final int PERSON_ID_COLUMN_INDEX = 0;
+    protected static final int PERSON_DISPLAY_NAME_COLUMN_INDEX = 1;
+    protected static final int PERSON_PHONETIC_NAME_COLUMN_INDEX = 2;
+    protected static final int PERSON_STARRED_COLUMN_INDEX = 3;
+    protected static final int PERSON_PRESENCE_STATUS_COLUMN_INDEX = 4;
+
+    private CharSequence mUnknownNameText;
+
+    public LegacyContactListAdapter(Context context) {
+        super(context);
+        mUnknownNameText = context.getText(android.R.string.unknownName);
+    }
+
+    @Override
+    public void configureLoader(CursorLoader loader) {
+        loader.setUri(People.CONTENT_URI);
+        loader.setProjection(PEOPLE_PROJECTION);
+        loader.setSortOrder(People.DISPLAY_NAME);
+    }
+
+    public boolean isContactStarred() {
+        return getCursor().getInt(PERSON_STARRED_COLUMN_INDEX) != 0;
+    }
+
+    @Override
+    public String getContactDisplayName() {
+        return getCursor().getString(PERSON_DISPLAY_NAME_COLUMN_INDEX);
+    }
+
+    public Uri getPersonUri() {
+        Cursor cursor = getCursor();
+        long personId = cursor.getLong(PERSON_ID_COLUMN_INDEX);
+        return ContentUris.withAppendedId(People.CONTENT_URI, personId);
+    }
+
+    @Override
+    public View newView(Context context, Cursor cursor, ViewGroup parent) {
+        final ContactListItemView view = new ContactListItemView(context, null);
+        view.setUnknownNameText(mUnknownNameText);
+        return view;
+    }
+
+    @Override
+    public void bindView(View itemView, Context context, Cursor cursor) {
+        ContactListItemView view = (ContactListItemView)itemView;
+        bindName(view, cursor);
+        bindPresence(view, cursor);
+    }
+
+    protected void bindName(final ContactListItemView view, Cursor cursor) {
+        view.showDisplayName(cursor, PERSON_DISPLAY_NAME_COLUMN_INDEX, false, 0);
+        view.showPhoneticName(cursor, PERSON_PHONETIC_NAME_COLUMN_INDEX);
+    }
+
+    protected void bindPresence(final ContactListItemView view, Cursor cursor) {
+        view.showPresence(cursor, PERSON_PRESENCE_STATUS_COLUMN_INDEX);
+    }
+}
diff --git a/src/com/android/contacts/list/LegacyPhoneNumberListAdapter.java b/src/com/android/contacts/list/LegacyPhoneNumberListAdapter.java
new file mode 100644
index 0000000..a98ed07
--- /dev/null
+++ b/src/com/android/contacts/list/LegacyPhoneNumberListAdapter.java
@@ -0,0 +1,107 @@
+/*
+ * 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.list;
+
+import android.app.patterns.CursorLoader;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.Contacts.People;
+import android.provider.Contacts.Phones;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A cursor adapter for the Phones.CONTENT_TYPE content type.
+ */
+@SuppressWarnings("deprecation")
+public class LegacyPhoneNumberListAdapter extends ContactEntryListAdapter {
+
+    private static final String[] PHONES_PROJECTION = new String[] {
+        Phones._ID,             // 0
+        Phones.TYPE,            // 1
+        Phones.LABEL,           // 2
+        Phones.NUMBER,          // 3
+        People.DISPLAY_NAME,    // 4
+        People.PHONETIC_NAME,   // 5
+    };
+
+    private static final int PHONE_ID_COLUMN_INDEX = 0;
+    private static final int PHONE_TYPE_COLUMN_INDEX = 1;
+    private static final int PHONE_LABEL_COLUMN_INDEX = 2;
+    private static final int PHONE_NUMBER_COLUMN_INDEX = 3;
+    private static final int PHONE_DISPLAY_NAME_COLUMN_INDEX = 4;
+    private static final int PHONE_PHONETIC_NAME_COLUMN_INDEX = 5;
+
+    private CharSequence mUnknownNameText;
+
+    public LegacyPhoneNumberListAdapter(Context context) {
+        super(context);
+        mUnknownNameText = context.getText(android.R.string.unknownName);
+    }
+
+    @Override
+    public void configureLoader(CursorLoader loader) {
+        loader.setUri(Phones.CONTENT_URI);
+        loader.setProjection(PHONES_PROJECTION);
+        loader.setSortOrder(Phones.DISPLAY_NAME);
+    }
+
+    @Override
+    public String getContactDisplayName() {
+        return getCursor().getString(PHONE_DISPLAY_NAME_COLUMN_INDEX);
+    }
+
+    public Uri getPhoneUri() {
+        Cursor cursor = getCursor();
+        long id = cursor.getLong(PHONE_ID_COLUMN_INDEX);
+        return ContentUris.withAppendedId(Phones.CONTENT_URI, id);
+    }
+
+    @Override
+    public View newView(Context context, Cursor cursor, ViewGroup parent) {
+        final ContactListItemView view = new ContactListItemView(context, null);
+        view.setUnknownNameText(mUnknownNameText);
+        return view;
+    }
+
+    @Override
+    public void bindView(View itemView, Context context, Cursor cursor) {
+        ContactListItemView view = (ContactListItemView)itemView;
+        bindName(view, cursor);
+        bindPhoneNumber(view, cursor);
+    }
+
+    protected void bindName(final ContactListItemView view, Cursor cursor) {
+        view.showDisplayName(cursor, PHONE_DISPLAY_NAME_COLUMN_INDEX, false, 0);
+        view.showPhoneticName(cursor, PHONE_PHONETIC_NAME_COLUMN_INDEX);
+    }
+
+    protected void bindPhoneNumber(ContactListItemView view, Cursor cursor) {
+        CharSequence label = null;
+        if (!cursor.isNull(PHONE_TYPE_COLUMN_INDEX)) {
+            final int type = cursor.getInt(PHONE_TYPE_COLUMN_INDEX);
+            final String customLabel = cursor.getString(PHONE_LABEL_COLUMN_INDEX);
+
+            // TODO cache
+            label = Phone.getTypeLabel(getContext().getResources(), type, customLabel);
+        }
+        view.setLabel(label);
+        view.showData(cursor, PHONE_NUMBER_COLUMN_INDEX);
+    }
+}
diff --git a/src/com/android/contacts/list/LegacyPostalAddressListAdapter.java b/src/com/android/contacts/list/LegacyPostalAddressListAdapter.java
new file mode 100644
index 0000000..1eff102
--- /dev/null
+++ b/src/com/android/contacts/list/LegacyPostalAddressListAdapter.java
@@ -0,0 +1,108 @@
+/*
+ * 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.list;
+
+import android.app.patterns.CursorLoader;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.Contacts.ContactMethods;
+import android.provider.Contacts.People;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A cursor adapter for the ContactMethods.CONTENT_TYPE content type.
+ */
+@SuppressWarnings("deprecation")
+public class LegacyPostalAddressListAdapter extends ContactEntryListAdapter {
+
+    static final String[] POSTALS_PROJECTION = new String[] {
+        ContactMethods._ID,     // 0
+        ContactMethods.TYPE,    // 1
+        ContactMethods.LABEL,   // 2
+        ContactMethods.DATA,    // 3
+        People.DISPLAY_NAME,    // 4
+        People.PHONETIC_NAME,   // 5
+    };
+
+    public static final int POSTAL_ID_COLUMN_INDEX = 0;
+    public static final int POSTAL_TYPE_COLUMN_INDEX = 1;
+    public static final int POSTAL_LABEL_COLUMN_INDEX = 2;
+    public static final int POSTAL_NUMBER_COLUMN_INDEX = 3;
+    public static final int POSTAL_DISPLAY_NAME_COLUMN_INDEX = 4;
+    public static final int POSTAL_PHONETIC_NAME_COLUMN_INDEX = 5;
+
+    private CharSequence mUnknownNameText;
+
+    public LegacyPostalAddressListAdapter(Context context) {
+        super(context);
+        mUnknownNameText = context.getText(android.R.string.unknownName);
+    }
+
+    @Override
+    public void configureLoader(CursorLoader loader) {
+        loader.setUri(ContactMethods.CONTENT_URI);
+        loader.setProjection(POSTALS_PROJECTION);
+        loader.setSortOrder(People.DISPLAY_NAME);
+        loader.setSelection(ContactMethods.KIND + "=" + android.provider.Contacts.KIND_POSTAL);
+    }
+
+    @Override
+    public String getContactDisplayName() {
+        return getCursor().getString(POSTAL_DISPLAY_NAME_COLUMN_INDEX);
+    }
+
+    public Uri getContactMethodUri() {
+        Cursor cursor = getCursor();
+        long id = cursor.getLong(POSTAL_ID_COLUMN_INDEX);
+        return ContentUris.withAppendedId(ContactMethods.CONTENT_URI, id);
+    }
+
+    @Override
+    public View newView(Context context, Cursor cursor, ViewGroup parent) {
+        final ContactListItemView view = new ContactListItemView(context, null);
+        view.setUnknownNameText(mUnknownNameText);
+        return view;
+    }
+
+    @Override
+    public void bindView(View itemView, Context context, Cursor cursor) {
+        ContactListItemView view = (ContactListItemView)itemView;
+        bindName(view, cursor);
+        bindPostalAddress(view, cursor);
+    }
+
+    protected void bindName(final ContactListItemView view, Cursor cursor) {
+        view.showDisplayName(cursor, POSTAL_DISPLAY_NAME_COLUMN_INDEX, false, 0);
+        view.showPhoneticName(cursor, POSTAL_PHONETIC_NAME_COLUMN_INDEX);
+    }
+
+    protected void bindPostalAddress(ContactListItemView view, Cursor cursor) {
+        CharSequence label = null;
+        if (!cursor.isNull(POSTAL_TYPE_COLUMN_INDEX)) {
+            final int type = cursor.getInt(POSTAL_TYPE_COLUMN_INDEX);
+            final String customLabel = cursor.getString(POSTAL_LABEL_COLUMN_INDEX);
+
+            // TODO cache
+            label = StructuredPostal.getTypeLabel(getContext().getResources(), type, customLabel);
+        }
+        view.setLabel(label);
+        view.showData(cursor, POSTAL_NUMBER_COLUMN_INDEX);
+    }
+}
diff --git a/src/com/android/contacts/list/MultiplePhonePickerAdapter.java b/src/com/android/contacts/list/MultiplePhonePickerAdapter.java
new file mode 100644
index 0000000..16ec183
--- /dev/null
+++ b/src/com/android/contacts/list/MultiplePhonePickerAdapter.java
@@ -0,0 +1,363 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.text.TextUtils;
+import android.util.SparseIntArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+
+/**
+ * List adapter for the multiple phone picker.
+ */
+public class MultiplePhonePickerAdapter extends PhoneNumberListAdapter {
+
+    public interface OnSelectionChangeListener {
+        void onSelectionChange();
+    }
+
+    private static final int[] CHIP_COLOR_ARRAY = {
+        R.drawable.appointment_indicator_leftside_1,
+        R.drawable.appointment_indicator_leftside_2,
+        R.drawable.appointment_indicator_leftside_3,
+        R.drawable.appointment_indicator_leftside_4,
+        R.drawable.appointment_indicator_leftside_5,
+        R.drawable.appointment_indicator_leftside_6,
+        R.drawable.appointment_indicator_leftside_7,
+        R.drawable.appointment_indicator_leftside_8,
+        R.drawable.appointment_indicator_leftside_9,
+        R.drawable.appointment_indicator_leftside_10,
+        R.drawable.appointment_indicator_leftside_11,
+        R.drawable.appointment_indicator_leftside_12,
+        R.drawable.appointment_indicator_leftside_13,
+        R.drawable.appointment_indicator_leftside_14,
+        R.drawable.appointment_indicator_leftside_15,
+        R.drawable.appointment_indicator_leftside_16,
+        R.drawable.appointment_indicator_leftside_17,
+        R.drawable.appointment_indicator_leftside_18,
+        R.drawable.appointment_indicator_leftside_19,
+        R.drawable.appointment_indicator_leftside_20,
+        R.drawable.appointment_indicator_leftside_21,
+    };
+
+    public static final long INVALID_PHONE_ID = -1;
+
+    /** The phone numbers */
+    private ArrayList<String> mPhoneNumbers = new ArrayList<String>();
+
+    /** The selected phone numbers in the PhoneNumberAdapter */
+    private HashSet<String> mSelectedPhoneNumbers = new HashSet<String>();
+
+    /** The phone numbers after the filtering */
+    private ArrayList<String> mFilteredPhoneNumbers = new ArrayList<String>();
+
+    /** The PHONE_ID of selected number in user contacts*/
+    private HashSet<Long> mSelectedPhoneIds = new HashSet<Long>();
+
+    private boolean mSelectionChanged;
+
+    private OnSelectionChangeListener mSelectionChangeListener;
+
+    /**
+     * This is a map from contact ID to color index. A colored chip is used to
+     * indicate the number of phone numbers belong to one contact
+     */
+    private SparseIntArray mContactColor = new SparseIntArray();
+
+    public MultiplePhonePickerAdapter(Context context) {
+        super(context);
+    }
+
+    public void setOnSelectionChangeListener(OnSelectionChangeListener listener) {
+        this.mSelectionChangeListener = listener;
+    }
+
+    public void setPhoneNumbers(ArrayList<String> phoneNumbers) {
+        mPhoneNumbers.clear();
+        mPhoneNumbers.addAll(phoneNumbers);
+    }
+
+    public int getSelectedCount() {
+        return mSelectedPhoneNumbers.size() + mSelectedPhoneIds.size();
+    }
+
+    public Uri[] getSelectedUris() {
+        Uri[] uris = new Uri[mSelectedPhoneNumbers.size() + mSelectedPhoneIds.size()];
+        int count = mPhoneNumbers.size();
+        int index = 0;
+        for (int i = 0; i < count; i++) {
+            String phoneNumber = mPhoneNumbers.get(i);
+            if (isSelected(phoneNumber)) {
+                uris[index++] = Uri.parse("tel:" + phoneNumber);
+            }
+        }
+        for (Long contactId : mSelectedPhoneIds) {
+            uris[index++] = ContentUris.withAppendedId(Phone.CONTENT_URI, contactId);
+        }
+        return uris;
+    }
+
+    public void setSelectedUris(Uri[] uris) {
+        mSelectedPhoneNumbers.clear();
+        mSelectedPhoneIds.clear();
+        if (uris != null) {
+            for (Uri uri : uris) {
+                String scheme = uri.getScheme();
+                if ("tel".equals(scheme)) {
+                    String phoneNumber = uri.getSchemeSpecificPart();
+                    if (!mPhoneNumbers.contains(phoneNumber)) {
+                        mPhoneNumbers.add(phoneNumber);
+                    }
+                    mSelectedPhoneNumbers.add(phoneNumber);
+            } else if ("content".equals(scheme)) {
+                mSelectedPhoneIds.add(ContentUris.parseId(uri));
+            }
+            }
+        }
+        mFilteredPhoneNumbers.clear();
+        mFilteredPhoneNumbers.addAll(mPhoneNumbers);
+    }
+
+    public void toggleSelection(int position) {
+        if (position < mFilteredPhoneNumbers.size()) {
+            String phoneNumber = mPhoneNumbers.get(position);
+            setPhoneSelected(phoneNumber, !isSelected(phoneNumber));
+        } else {
+            Cursor cursor = getCursor();
+            cursor.moveToPosition(position - mFilteredPhoneNumbers.size());
+            long phoneId = cursor.getLong(PHONE_ID_COLUMN_INDEX);
+            setPhoneSelected(phoneId, !isSelected(phoneId));
+        }
+        notifyDataSetChanged();
+    }
+
+    public boolean isSelectionChanged() {
+        return mSelectionChanged;
+    }
+
+    public void setSelectionChanged(boolean flag) {
+        mSelectionChanged = flag;
+        if (mSelectionChangeListener != null) {
+            mSelectionChangeListener.onSelectionChange();
+        }
+    }
+
+    public void setPhoneSelected(final String phoneNumber, boolean selected) {
+        if (!TextUtils.isEmpty(phoneNumber)) {
+            if (selected) {
+                mSelectedPhoneNumbers.add(phoneNumber);
+            } else {
+                mSelectedPhoneNumbers.remove(phoneNumber);
+            }
+        }
+        setSelectionChanged(true);
+    }
+
+    public void setPhoneSelected(long phoneId, boolean selected) {
+        if (selected) {
+            mSelectedPhoneIds.add(phoneId);
+        } else {
+            mSelectedPhoneIds.remove(phoneId);
+        }
+        setSelectionChanged(true);
+    }
+
+    public boolean isSelected(long phoneId) {
+        return mSelectedPhoneIds.contains(phoneId);
+    }
+
+    public boolean isSelected(final String phoneNumber) {
+        return mSelectedPhoneNumbers.contains(phoneNumber);
+    }
+
+    public void setAllPhonesSelected(boolean selected) {
+//        if (selected) {
+//            Cursor cursor = this.mMultiplePhoneSelectionActivity.mAdapter.getCursor();
+//            if (cursor != null) {
+//                int backupPos = cursor.getPosition();
+//                cursor.moveToPosition(-1);
+//                while (cursor.moveToNext()) {
+//                    setPhoneSelected(cursor
+//                            .getLong(MultiplePhonePickerActivity.PHONE_ID_COLUMN_INDEX), true);
+//                }
+//                cursor.moveToPosition(backupPos);
+//            }
+//            for (String number : this.mMultiplePhoneSelectionActivity.mPhoneNumberAdapter
+//                    .getFilteredPhoneNumbers()) {
+//                setPhoneSelected(number, true);
+//            }
+//        } else {
+//            mSelectedPhoneIds.clear();
+//            mSelectedPhoneNumbers.clear();
+//        }
+    }
+
+    public boolean isAllSelected() {
+        return false;
+//        return selectedCount() == this.mMultiplePhoneSelectionActivity.mPhoneNumberAdapter
+//                .getFilteredPhoneNumbers().size()
+//                + this.mMultiplePhoneSelectionActivity.mAdapter.getCount();
+    }
+
+    public Iterator<Long> getSelectedPhoneIds() {
+        return mSelectedPhoneIds.iterator();
+    }
+
+    @Override
+    public int getViewTypeCount() {
+        return 2;
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        return position < mPhoneNumbers.size() ? 0 : 1;
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        View view;
+        if (convertView == null || convertView.getTag() == null) {
+            view = newView(getContext(), null, parent);
+        } else {
+            view = convertView;
+        }
+
+        boolean showingSuggestion = false;
+
+        if (position < mFilteredPhoneNumbers.size()) {
+            bindExtraPhoneView(view, position);
+        } else {
+            Cursor cursor = getCursor();
+            cursor.moveToPosition(position - mFilteredPhoneNumbers.size());
+            bindView(view, getContext(), cursor);
+        }
+        return view;
+    }
+
+    @Override
+    public View newView(Context context, Cursor cursor, ViewGroup parent) {
+        final MultiplePhonePickerItemView view = new MultiplePhonePickerItemView(context, null);
+        view.setUnknownNameText(getUnknownNameText());
+        view.setTextWithHighlightingFactory(getTextWithHighlightingFactory());
+        return view;
+    }
+
+    private void bindExtraPhoneView(View itemView, int position) {
+        final MultiplePhonePickerItemView view = (MultiplePhonePickerItemView)itemView;
+        String phoneNumber = mFilteredPhoneNumbers.get(position);
+        view.getNameTextView().setText(phoneNumber);
+        CheckBox checkBox = view.getCheckBoxView();
+        checkBox.setChecked(isSelected(phoneNumber));
+        view.phoneId = INVALID_PHONE_ID;
+        view.phoneNumber = phoneNumber;
+    }
+
+    @Override
+    public void bindView(View itemView, Context context, Cursor cursor) {
+        super.bindView(itemView, context, cursor);
+
+        final MultiplePhonePickerItemView view = (MultiplePhonePickerItemView)itemView;
+        view.phoneId = Long.valueOf(cursor.getLong(PHONE_ID_COLUMN_INDEX));
+        CheckBox checkBox = view.getCheckBoxView();
+        checkBox.setChecked(isSelected(view.phoneId));
+
+        long contactId = cursor.getLong(PHONE_CONTACT_ID_COLUMN_INDEX);
+        view.getChipView().setBackgroundResource(getChipColor(contactId));
+    }
+
+//    @Override
+//    protected void prepareEmptyView() {
+//        mMultiplePhonePickerActivity.mEmptyView.show(mMultiplePhonePickerActivity.mSearchMode,
+//                true, false, false, false, true, mMultiplePhonePickerActivity.mShowSelectedOnly);
+//    }
+
+    /**
+     * Get assigned chip color resource id for a given contact, 0 is returned if there is no mapped
+     * resource.
+     */
+    public int getChipColor(long contactId) {
+        return mContactColor.get((int)contactId);
+    }
+
+    // TODO filtering
+//    public void doFilter(final String constraint, boolean selectedOnly) {
+//        if (mPhoneNumbers == null) {
+//            return;
+//        }
+//        mFilteredPhoneNumbers.clear();
+//        for (String number : mPhoneNumbers) {
+//            if (selectedOnly && !mSelection.isSelected(number) ||
+//                    !TextUtils.isEmpty(constraint) && !number.startsWith(constraint)) {
+//                continue;
+//            }
+//            mFilteredPhoneNumbers.add(number);
+//        }
+//    }
+
+    @Override
+    public int getCount() {
+        if (!mDataValid) {
+            return 0;
+        }
+
+        return super.getCount() + mFilteredPhoneNumbers.size();
+    }
+
+    @Override
+    public void changeCursor(Cursor cursor) {
+        super.changeCursor(cursor);
+        updateChipColor(cursor);
+    }
+
+    /**
+     * Go through the cursor and assign the chip color to contact who has more
+     * than one phone numbers. Assume the cursor is clustered by CONTACT_ID.
+     */
+    public void updateChipColor(Cursor cursor) {
+        if (cursor == null || cursor.getCount() == 0) {
+            return;
+        }
+        mContactColor.clear();
+        cursor.moveToFirst();
+        int colorIndex = 0;
+        long prevContactId = cursor.getLong(PHONE_CONTACT_ID_COLUMN_INDEX);
+        while (cursor.moveToNext()) {
+            long contactId = cursor.getLong(PHONE_CONTACT_ID_COLUMN_INDEX);
+            if (prevContactId == contactId) {
+                if (mContactColor.indexOfKey((int)contactId) < 0) {
+                    mContactColor.put((int)contactId, CHIP_COLOR_ARRAY[colorIndex]);
+                    colorIndex++;
+                    if (colorIndex >= CHIP_COLOR_ARRAY.length) {
+                        colorIndex = 0;
+                    }
+                }
+            }
+            prevContactId = contactId;
+        }
+    }
+}
diff --git a/src/com/android/contacts/list/MultiplePhonePickerFragment.java b/src/com/android/contacts/list/MultiplePhonePickerFragment.java
new file mode 100644
index 0000000..bf75a9f
--- /dev/null
+++ b/src/com/android/contacts/list/MultiplePhonePickerFragment.java
@@ -0,0 +1,181 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+import com.android.contacts.list.MultiplePhonePickerAdapter.OnSelectionChangeListener;
+
+import android.app.Activity;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.view.View.OnClickListener;
+import android.view.animation.AnimationUtils;
+import android.widget.Button;
+
+/**
+ * Fragment for the multiple phone picker.
+ */
+public class MultiplePhonePickerFragment
+        extends ContactEntryListFragment<MultiplePhonePickerAdapter>
+        implements OnClickListener, OnSelectionChangeListener {
+
+    private static final String SELECTION_EXTRA_KEY = "selection";
+    private static final String SELECTION_CHANGED_EXTRA_KEY = "selectionChanged";
+
+    private OnMultiplePhoneNumberPickerActionListener mListener;
+
+    /**
+     * UI control of action panel in MODE_PICK_MULTIPLE_PHONES mode.
+     */
+    private View mFooterView;
+
+    private Uri[] mSelectedUris;
+    private boolean mSelectionChanged;
+
+    public MultiplePhonePickerFragment() {
+        setSectionHeaderDisplayEnabled(false);
+        setPhotoLoaderEnabled(true);
+    }
+
+    public void setOnMultiplePhoneNumberPickerActionListener(
+            OnMultiplePhoneNumberPickerActionListener listener) {
+        mListener = listener;
+    }
+
+    public Uri[] getSelectedUris() {
+        return getAdapter().getSelectedUris();
+    }
+
+    public void setSelectedUris(Parcelable[] extras) {
+        Uri[] uris = new Uri[extras == null ? 0 : extras.length];
+        if (extras != null) {
+            for (int i = 0; i < extras.length; i++) {
+                uris[i] = (Uri)extras[i];
+            }
+        }
+        setSelectedUris(uris);
+    }
+
+    public void setSelectedUris(Uri[] uris) {
+        mSelectedUris = uris;
+        MultiplePhonePickerAdapter adapter = getAdapter();
+        if (adapter != null) {
+            adapter.setSelectedUris(uris);
+        }
+    }
+
+    @Override
+    protected MultiplePhonePickerAdapter createListAdapter() {
+        return new MultiplePhonePickerAdapter(getActivity());
+    }
+
+    @Override
+    protected void configureAdapter() {
+        super.configureAdapter();
+        MultiplePhonePickerAdapter adapter = getAdapter();
+        adapter.setSelectedUris(mSelectedUris);
+        adapter.setSelectionChanged(mSelectionChanged);
+        adapter.setOnSelectionChangeListener(this);
+    }
+
+    @Override
+    protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+        View view = inflater.inflate(R.layout.contacts_list_content, null);
+        ViewStub stub = (ViewStub)view.findViewById(R.id.footer_stub);
+        if (stub != null) {
+            View stubView = stub.inflate();
+            mFooterView = stubView.findViewById(R.id.footer);
+            mFooterView.setVisibility(View.GONE);
+            Button doneButton = (Button) stubView.findViewById(R.id.done);
+            doneButton.setOnClickListener(this);
+            Button revertButton = (Button) stubView.findViewById(R.id.revert);
+            revertButton.setOnClickListener(this);
+        }
+        return view;
+    }
+
+    @Override
+    protected void onItemClick(int position, long id) {
+        getAdapter().toggleSelection(position);
+    }
+
+    public void onClick(View v) {
+        int id = v.getId();
+        switch (id) {
+            case R.id.done:
+                mListener.onPhoneNumbersSelectedAction(getAdapter().getSelectedUris());
+                break;
+            case R.id.revert:
+                mListener.onFinishAction();
+                break;
+        }
+    }
+
+    @Override
+    public void onCreate(Bundle savedState) {
+        super.onCreate(savedState);
+        if (savedState != null) {
+            setSelectedUris(savedState.getParcelableArray(SELECTION_EXTRA_KEY));
+            mSelectionChanged = savedState.getBoolean(SELECTION_CHANGED_EXTRA_KEY, false);
+            if (getAdapter() != null) {
+                getAdapter().setSelectionChanged(mSelectionChanged);
+            }
+        }
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        updateWidgets();
+    }
+
+    public void onSelectionChange() {
+        updateWidgets();
+    }
+
+    private void updateWidgets() {
+        int selected = getAdapter().getSelectedCount();
+
+        Activity context = getActivity();
+        if (selected >= 1) {
+            final String format = context.getResources().getQuantityString(
+                    R.plurals.multiple_picker_title, selected);
+
+            // TODO: turn this into a callback
+            context.setTitle(String.format(format, selected));
+        } else {
+            // TODO: turn this into a callback
+            context.setTitle(context.getString(R.string.contactsList));
+        }
+
+        if (getAdapter().isSelectionChanged() && mFooterView.getVisibility() == View.GONE) {
+            mFooterView.setVisibility(View.VISIBLE);
+            mFooterView.startAnimation(AnimationUtils.loadAnimation(context, R.anim.footer_appear));
+        }
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle icicle) {
+        super.onSaveInstanceState(icicle);
+        icicle.putParcelableArray(SELECTION_EXTRA_KEY, getAdapter().getSelectedUris());
+        icicle.putBoolean(SELECTION_CHANGED_EXTRA_KEY, getAdapter().isSelectionChanged());
+    }
+}
diff --git a/src/com/android/contacts/list/MultiplePhonePickerItemView.java b/src/com/android/contacts/list/MultiplePhonePickerItemView.java
new file mode 100644
index 0000000..4801d33
--- /dev/null
+++ b/src/com/android/contacts/list/MultiplePhonePickerItemView.java
@@ -0,0 +1,121 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.CheckBox;
+
+/**
+ * A custom view for an item in the phone multi-picker list.
+ */
+public class MultiplePhonePickerItemView extends ContactListItemView {
+
+    // Used to indicate the sequence of phones belong to the same contact in multi-picker
+    private View mChipView;
+    // Used to select the phone in multi-picker
+    private CheckBox mCheckBox;
+
+    private int mChipWidth;
+    private int mChipRightMargin;
+    private int mCheckBoxMargin;
+
+    public long phoneId;
+    // phoneNumber only validates when phoneId = INVALID_PHONE_ID
+    public String phoneNumber;
+
+    public MultiplePhonePickerItemView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        Resources resources = context.getResources();
+        mChipWidth =
+                resources.getDimensionPixelOffset(R.dimen.list_item_header_chip_width);
+        mChipRightMargin =
+                resources.getDimensionPixelOffset(R.dimen.list_item_header_chip_right_margin);
+        mCheckBoxMargin =
+                resources.getDimensionPixelOffset(R.dimen.list_item_header_checkbox_margin);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        if (isVisible(mChipView)) {
+            mChipView.measure(0, 0);
+        }
+
+        if (isVisible(mCheckBox)) {
+            mCheckBox.measure(0, 0);
+        }
+
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+    }
+
+    @Override
+    protected int layoutLeftSide(int height, int topBound, int leftBound) {
+        if (mChipView != null) {
+            mChipView.layout(leftBound, topBound, leftBound + mChipWidth, height);
+            leftBound += mChipWidth + mChipRightMargin;
+        }
+
+        return super.layoutLeftSide(height, topBound, leftBound);
+    }
+
+    @Override
+    protected int layoutRightSide(int height, int topBound, int rightBound) {
+        rightBound = super.layoutRightSide(height, topBound, rightBound);
+
+        if (isVisible(mCheckBox)) {
+            int checkBoxWidth = mCheckBox.getMeasuredWidth();
+            int checkBoxHight = mCheckBox.getMeasuredHeight();
+            rightBound -= mCheckBoxMargin + checkBoxWidth;
+            int checkBoxTop = topBound + (height - topBound - checkBoxHight) / 2;
+            mCheckBox.layout(
+                    rightBound,
+                    checkBoxTop,
+                    rightBound + checkBoxWidth,
+                    checkBoxTop + checkBoxHight);
+        }
+
+        return rightBound;
+    }
+
+    /**
+     * Returns the chip view for the multipicker, creating it if necessary.
+     */
+    public View getChipView() {
+        if (mChipView == null) {
+            mChipView = new View(mContext);
+            addView(mChipView);
+        }
+        return mChipView;
+    }
+
+    /**
+     * Returns the CheckBox view for the multipicker, creating it if necessary.
+     */
+    public CheckBox getCheckBoxView() {
+        if (mCheckBox == null) {
+            mCheckBox = new CheckBox(mContext);
+            mCheckBox.setClickable(false);
+            mCheckBox.setFocusable(false);
+            addView(mCheckBox);
+        }
+        return mCheckBox;
+    }
+}
diff --git a/src/com/android/contacts/list/OnContactBrowserActionListener.java b/src/com/android/contacts/list/OnContactBrowserActionListener.java
new file mode 100644
index 0000000..56f9bbc
--- /dev/null
+++ b/src/com/android/contacts/list/OnContactBrowserActionListener.java
@@ -0,0 +1,74 @@
+/*
+ * 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.list;
+
+import android.net.Uri;
+
+/**
+ * Action callbacks that can be sent by a contact list.
+ */
+public interface OnContactBrowserActionListener  {
+
+    /**
+     * Searches all contacts for the specified string an show results for browsing.
+     */
+    void onSearchAllContactsAction(String string);
+
+    /**
+     * Opens the specified contact for viewing.
+     */
+    void onViewContactAction(Uri contactLookupUri);
+
+    /**
+     * Creates a new contact.
+     */
+    void onCreateNewContactAction();
+
+    /**
+     * Opens the specified contact for editing.
+     */
+    void onEditContactAction(Uri contactLookupUri);
+
+    /**
+     * Initiates the contact deletion process.
+     */
+    void onDeleteContactAction(Uri contactUri);
+
+    /**
+     * Adds the specified contact to favorites
+     */
+    void onAddToFavoritesAction(Uri contactUri);
+
+    /**
+     * Removes the specified contact from favorites.
+     */
+    void onRemoveFromFavoritesAction(Uri contactUri);
+
+    /**
+     * Places a call to the specified contact.
+     */
+    void onCallContactAction(Uri contactUri);
+
+    /**
+     * Initiates a text message to the specified contact.
+     */
+    void onSmsContactAction(Uri contactUri);
+
+    /**
+     * Closes the contact browser.
+     */
+    void onFinishAction();
+}
diff --git a/src/com/android/contacts/list/OnContactPickerActionListener.java b/src/com/android/contacts/list/OnContactPickerActionListener.java
new file mode 100644
index 0000000..7245fbc
--- /dev/null
+++ b/src/com/android/contacts/list/OnContactPickerActionListener.java
@@ -0,0 +1,45 @@
+/*
+ * 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.list;
+
+import android.content.Intent;
+import android.net.Uri;
+
+/**
+ * Action callbacks that can be sent by a contact picker.
+ */
+public interface OnContactPickerActionListener  {
+
+    /**
+     * Searches all contacts for the specified string an show results for browsing.
+     */
+    void onSearchAllContactsAction(String string);
+
+    /**
+     * Creates a new contact and then returns it to the caller.
+     */
+    void onCreateNewContactAction();
+
+    /**
+     * Returns the selected contact to the requester.
+     */
+    void onPickContactAction(Uri contactUri);
+
+    /**
+     * Returns the selected contact as a shortcut intent.
+     */
+    void onShortcutIntentCreated(Intent intent);
+}
diff --git a/src/com/android/contacts/list/OnMultiplePhoneNumberPickerActionListener.java b/src/com/android/contacts/list/OnMultiplePhoneNumberPickerActionListener.java
new file mode 100644
index 0000000..ac010ba
--- /dev/null
+++ b/src/com/android/contacts/list/OnMultiplePhoneNumberPickerActionListener.java
@@ -0,0 +1,34 @@
+/*
+ * 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.list;
+
+import android.net.Uri;
+
+/**
+ * Action callbacks that can be sent by a multiple phone number picker.
+ */
+public interface OnMultiplePhoneNumberPickerActionListener {
+
+    /**
+     * Returns the selected phone numbers to the requester.
+     */
+    void onPhoneNumbersSelectedAction(Uri[] dataUris);
+
+    /**
+     * Closes the picker without changing the selection.
+     */
+    void onFinishAction();
+}
diff --git a/src/com/android/contacts/list/OnPhoneNumberPickerActionListener.java b/src/com/android/contacts/list/OnPhoneNumberPickerActionListener.java
new file mode 100644
index 0000000..701cc78
--- /dev/null
+++ b/src/com/android/contacts/list/OnPhoneNumberPickerActionListener.java
@@ -0,0 +1,40 @@
+/*
+ * 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.list;
+
+import android.content.Intent;
+import android.net.Uri;
+
+/**
+ * Action callbacks that can be sent by a phone number picker.
+ */
+public interface OnPhoneNumberPickerActionListener  {
+
+    /**
+     * Returns the selected phone number to the requester.
+     */
+    void onPickPhoneNumberAction(Uri dataUri);
+
+    /**
+     * Returns the selected number as a shortcut intent.
+     */
+    void onShortcutIntentCreated(Intent intent);
+
+    /**
+     * Searches all contacts for the specified string an show results for browsing.
+     */
+    void onSearchAllContactsAction(String string);
+}
diff --git a/src/com/android/contacts/list/OnPostalAddressPickerActionListener.java b/src/com/android/contacts/list/OnPostalAddressPickerActionListener.java
new file mode 100644
index 0000000..a43dfe4
--- /dev/null
+++ b/src/com/android/contacts/list/OnPostalAddressPickerActionListener.java
@@ -0,0 +1,34 @@
+/*
+ * 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.list;
+
+import android.net.Uri;
+
+/**
+ * Action callbacks that can be sent by a postal address picker.
+ */
+public interface OnPostalAddressPickerActionListener  {
+
+    /**
+     * Returns the selected phone number to the requester.
+     */
+    void onPickPostalAddressAction(Uri dataUri);
+
+    /**
+     * Searches all contacts for the specified string an show results for browsing.
+     */
+    void onSearchAllContactsAction(String string);
+}
diff --git a/src/com/android/contacts/list/PhoneNumberListAdapter.java b/src/com/android/contacts/list/PhoneNumberListAdapter.java
new file mode 100644
index 0000000..ffac30b
--- /dev/null
+++ b/src/com/android/contacts/list/PhoneNumberListAdapter.java
@@ -0,0 +1,184 @@
+/*
+ * 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.list;
+
+import android.app.patterns.CursorLoader;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.ContactCounts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A cursor adapter for the {@link Phone#CONTENT_TYPE} content type.
+ */
+public class PhoneNumberListAdapter extends ContactEntryListAdapter {
+
+    protected static final String[] PHONES_PROJECTION = new String[] {
+        Phone._ID,                          // 0
+        Phone.TYPE,                         // 1
+        Phone.LABEL,                        // 2
+        Phone.NUMBER,                       // 3
+        Phone.DISPLAY_NAME_PRIMARY,         // 4
+        Phone.DISPLAY_NAME_ALTERNATIVE,     // 5
+        Phone.CONTACT_ID,                   // 6
+        Phone.PHOTO_ID,                     // 7
+        Phone.PHONETIC_NAME,                // 8
+    };
+
+    protected static final int PHONE_ID_COLUMN_INDEX = 0;
+    protected static final int PHONE_TYPE_COLUMN_INDEX = 1;
+    protected static final int PHONE_LABEL_COLUMN_INDEX = 2;
+    protected static final int PHONE_NUMBER_COLUMN_INDEX = 3;
+    protected static final int PHONE_PRIMARY_DISPLAY_NAME_COLUMN_INDEX = 4;
+    protected static final int PHONE_ALTERNATIVE_DISPLAY_NAME_COLUMN_INDEX = 5;
+    protected static final int PHONE_CONTACT_ID_COLUMN_INDEX = 6;
+    protected static final int PHONE_PHOTO_ID_COLUMN_INDEX = 7;
+    protected static final int PHONE_PHONETIC_NAME_COLUMN_INDEX = 8;
+
+    private CharSequence mUnknownNameText;
+    private int mDisplayNameColumnIndex;
+    private int mAlternativeDisplayNameColumnIndex;
+
+    public PhoneNumberListAdapter(Context context) {
+        super(context);
+
+        mUnknownNameText = context.getText(android.R.string.unknownName);
+    }
+
+    protected CharSequence getUnknownNameText() {
+        return mUnknownNameText;
+    }
+
+    @Override
+    public void configureLoader(CursorLoader loader) {
+        loader.setUri(buildSectionIndexerUri(Phone.CONTENT_URI));
+        loader.setProjection(PHONES_PROJECTION);
+
+        if (getSortOrder() == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
+            loader.setSortOrder(Phone.SORT_KEY_PRIMARY);
+        } else {
+            loader.setSortOrder(Phone.SORT_KEY_ALTERNATIVE);
+        }
+    }
+
+    protected static Uri buildSectionIndexerUri(Uri uri) {
+        return uri.buildUpon()
+                .appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true").build();
+    }
+
+    @Override
+    public String getContactDisplayName() {
+        return getCursor().getString(mDisplayNameColumnIndex);
+    }
+
+    @Override
+    public void setContactNameDisplayOrder(int displayOrder) {
+        super.setContactNameDisplayOrder(displayOrder);
+        if (getContactNameDisplayOrder() == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
+            mDisplayNameColumnIndex = PHONE_PRIMARY_DISPLAY_NAME_COLUMN_INDEX;
+            mAlternativeDisplayNameColumnIndex = PHONE_ALTERNATIVE_DISPLAY_NAME_COLUMN_INDEX;
+        } else {
+            mDisplayNameColumnIndex = PHONE_ALTERNATIVE_DISPLAY_NAME_COLUMN_INDEX;
+            mAlternativeDisplayNameColumnIndex = PHONE_PRIMARY_DISPLAY_NAME_COLUMN_INDEX;
+        }
+    }
+
+    /**
+     * Builds a {@link Data#CONTENT_URI} for the current cursor
+     * position.
+     */
+    public Uri getDataUri() {
+        Cursor cursor = getCursor();
+        long id = cursor.getLong(PHONE_ID_COLUMN_INDEX);
+        return ContentUris.withAppendedId(Data.CONTENT_URI, id);
+    }
+
+    @Override
+    public View newView(Context context, Cursor cursor, ViewGroup parent) {
+        final ContactListItemView view = new ContactListItemView(context, null);
+        view.setUnknownNameText(mUnknownNameText);
+        view.setTextWithHighlightingFactory(getTextWithHighlightingFactory());
+        return view;
+    }
+
+    @Override
+    public void bindView(View itemView, Context context, Cursor cursor) {
+        ContactListItemView view = (ContactListItemView)itemView;
+        bindSectionHeaderAndDivider(view, cursor);
+        bindName(view, cursor);
+        bindPhoto(view, cursor);
+        bindPhoneNumber(view, cursor);
+    }
+
+    protected void bindPhoneNumber(ContactListItemView view, Cursor cursor) {
+        CharSequence label = null;
+        if (!cursor.isNull(PHONE_TYPE_COLUMN_INDEX)) {
+            final int type = cursor.getInt(PHONE_TYPE_COLUMN_INDEX);
+            final String customLabel = cursor.getString(PHONE_LABEL_COLUMN_INDEX);
+
+            // TODO cache
+            label = Phone.getTypeLabel(getContext().getResources(), type, customLabel);
+        }
+        view.setLabel(label);
+        view.showData(cursor, PHONE_NUMBER_COLUMN_INDEX);
+    }
+
+    protected void bindSectionHeaderAndDivider(final ContactListItemView view, Cursor cursor) {
+        final int position = cursor.getPosition();
+        final int section = getSectionForPosition(position);
+        if (getPositionForSection(section) == position) {
+            String title = (String)getSections()[section];
+            view.setSectionHeader(title);
+        } else {
+            view.setDividerVisible(false);
+            view.setSectionHeader(null);
+        }
+
+        // move the divider for the last item in a section
+        if (getPositionForSection(section + 1) - 1 == position) {
+            view.setDividerVisible(false);
+        } else {
+            view.setDividerVisible(true);
+        }
+    }
+
+    protected void bindName(final ContactListItemView view, Cursor cursor) {
+        view.showDisplayName(cursor, mDisplayNameColumnIndex, isNameHighlightingEnabled(),
+                mAlternativeDisplayNameColumnIndex);
+        view.showPhoneticName(cursor, PHONE_PHONETIC_NAME_COLUMN_INDEX);
+    }
+
+    protected void bindPhoto(final ContactListItemView view, Cursor cursor) {
+        long photoId = 0;
+        if (!cursor.isNull(PHONE_PHOTO_ID_COLUMN_INDEX)) {
+            photoId = cursor.getLong(PHONE_PHOTO_ID_COLUMN_INDEX);
+        }
+
+        getPhotoLoader().loadPhoto(view.getPhotoView(), photoId);
+    }
+//
+//    protected void bindSearchSnippet(final ContactListItemView view, Cursor cursor) {
+//        view.showSnippet(cursor, SUMMARY_SNIPPET_MIMETYPE_COLUMN_INDEX,
+//                SUMMARY_SNIPPET_DATA1_COLUMN_INDEX, SUMMARY_SNIPPET_DATA4_COLUMN_INDEX);
+//    }
+
+}
diff --git a/src/com/android/contacts/list/PhoneNumberPickerFragment.java b/src/com/android/contacts/list/PhoneNumberPickerFragment.java
new file mode 100644
index 0000000..6e2bb31
--- /dev/null
+++ b/src/com/android/contacts/list/PhoneNumberPickerFragment.java
@@ -0,0 +1,110 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+import com.android.contacts.list.ShortcutIntentBuilder.OnShortcutIntentCreatedListener;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Fragment containing a phone number list for picking.
+ */
+public class PhoneNumberPickerFragment extends ContactEntryListFragment<ContactEntryListAdapter>
+        implements OnShortcutIntentCreatedListener {
+    private OnPhoneNumberPickerActionListener mListener;
+    private String mShortcutAction;
+
+    public PhoneNumberPickerFragment() {
+        setPhotoLoaderEnabled(true);
+        setSectionHeaderDisplayEnabled(true);
+    }
+
+    public void setOnPhoneNumberPickerActionListener(OnPhoneNumberPickerActionListener listener) {
+        this.mListener = listener;
+    }
+
+    /**
+     * @param shortcutAction either {@link Intent#ACTION_CALL} or
+     *            {@link Intent#ACTION_SENDTO} or null.
+     */
+    public void setShortcutAction(String shortcutAction) {
+        this.mShortcutAction = shortcutAction;
+    }
+
+    @Override
+    protected void onItemClick(int position, long id) {
+        if (!isLegacyCompatibilityMode()) {
+            PhoneNumberListAdapter adapter = (PhoneNumberListAdapter)getAdapter();
+//          if (adapter.isSearchAllContactsItemPosition(position)) {
+//              searchAllContacts();
+//          } else {
+            adapter.moveToPosition(position);
+            pickPhoneNumber(adapter.getDataUri());
+//          }
+        } else {
+            LegacyPhoneNumberListAdapter adapter = (LegacyPhoneNumberListAdapter)getAdapter();
+            adapter.moveToPosition(position);
+            pickPhoneNumber(adapter.getPhoneUri());
+        }
+    }
+
+    @Override
+    protected ContactEntryListAdapter createListAdapter() {
+        if (!isLegacyCompatibilityMode()) {
+            PhoneNumberListAdapter adapter = new PhoneNumberListAdapter(getActivity());
+            adapter.setSectionHeaderDisplayEnabled(true);
+            adapter.setDisplayPhotos(true);
+            return adapter;
+        } else {
+            LegacyPhoneNumberListAdapter adapter = new LegacyPhoneNumberListAdapter(getActivity());
+            adapter.setSectionHeaderDisplayEnabled(true);
+            adapter.setDisplayPhotos(true);
+            return adapter;
+        }
+    }
+
+    @Override
+    protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+        if (isSearchMode()) {
+            return inflater.inflate(R.layout.contacts_search_content, null);
+        } else if (isSearchResultsMode()) {
+            return inflater.inflate(R.layout.contacts_list_search_results, null);
+        } else {
+            return inflater.inflate(R.layout.contacts_list_content, null);
+        }
+    }
+
+    public void pickPhoneNumber(Uri uri) {
+        if (mShortcutAction == null) {
+            mListener.onPickPhoneNumberAction(uri);
+        } else {
+            if (isLegacyCompatibilityMode()) {
+                throw new UnsupportedOperationException();
+            }
+            ShortcutIntentBuilder builder = new ShortcutIntentBuilder(getActivity(), this);
+            builder.createPhoneNumberShortcutIntent(uri, mShortcutAction);
+        }
+    }
+
+    public void onShortcutIntentCreated(Uri uri, Intent shortcutIntent) {
+        mListener.onShortcutIntentCreated(shortcutIntent);
+    }
+}
diff --git a/src/com/android/contacts/list/PostalAddressListAdapter.java b/src/com/android/contacts/list/PostalAddressListAdapter.java
new file mode 100644
index 0000000..1b645fe
--- /dev/null
+++ b/src/com/android/contacts/list/PostalAddressListAdapter.java
@@ -0,0 +1,177 @@
+/*
+ * 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.list;
+
+import android.app.patterns.CursorLoader;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.ContactCounts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A cursor adapter for the {@link StructuredPostal#CONTENT_TYPE} content type.
+ */
+public class PostalAddressListAdapter extends ContactEntryListAdapter {
+
+    static final String[] POSTALS_PROJECTION = new String[] {
+        StructuredPostal._ID,                       // 0
+        StructuredPostal.TYPE,                      // 1
+        StructuredPostal.LABEL,                     // 2
+        StructuredPostal.DATA,                      // 3
+        StructuredPostal.DISPLAY_NAME_PRIMARY,      // 4
+        StructuredPostal.DISPLAY_NAME_ALTERNATIVE,  // 5
+        StructuredPostal.PHOTO_ID,                  // 6
+    };
+
+    protected static final int POSTAL_ID_COLUMN_INDEX = 0;
+    protected static final int POSTAL_TYPE_COLUMN_INDEX = 1;
+    protected static final int POSTAL_LABEL_COLUMN_INDEX = 2;
+    protected static final int POSTAL_ADDRESS_COLUMN_INDEX = 3;
+    protected static final int POSTAL_PRIMARY_DISPLAY_NAME_COLUMN_INDEX = 4;
+    protected static final int POSTAL_ALTERNATIVE_DISPLAY_NAME_COLUMN_INDEX = 5;
+    protected static final int POSTAL_PHOTO_ID_COLUMN_INDEX = 6;
+
+    private CharSequence mUnknownNameText;
+    private int mDisplayNameColumnIndex;
+    private int mAlternativeDisplayNameColumnIndex;
+
+    public PostalAddressListAdapter(Context context) {
+        super(context);
+
+        mUnknownNameText = context.getText(android.R.string.unknownName);
+    }
+
+    @Override
+    public void configureLoader(CursorLoader loader) {
+        loader.setUri(buildSectionIndexerUri(StructuredPostal.CONTENT_URI));
+        loader.setProjection(POSTALS_PROJECTION);
+
+        if (getSortOrder() == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
+            loader.setSortOrder(StructuredPostal.SORT_KEY_PRIMARY);
+        } else {
+            loader.setSortOrder(StructuredPostal.SORT_KEY_ALTERNATIVE);
+        }
+    }
+
+    protected static Uri buildSectionIndexerUri(Uri uri) {
+        return uri.buildUpon()
+                .appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true").build();
+    }
+
+    @Override
+    public String getContactDisplayName() {
+        return getCursor().getString(mDisplayNameColumnIndex);
+    }
+
+    @Override
+    public void setContactNameDisplayOrder(int displayOrder) {
+        super.setContactNameDisplayOrder(displayOrder);
+        if (getContactNameDisplayOrder() == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
+            mDisplayNameColumnIndex = POSTAL_PRIMARY_DISPLAY_NAME_COLUMN_INDEX;
+            mAlternativeDisplayNameColumnIndex = POSTAL_ALTERNATIVE_DISPLAY_NAME_COLUMN_INDEX;
+        } else {
+            mDisplayNameColumnIndex = POSTAL_ALTERNATIVE_DISPLAY_NAME_COLUMN_INDEX;
+            mAlternativeDisplayNameColumnIndex = POSTAL_PRIMARY_DISPLAY_NAME_COLUMN_INDEX;
+        }
+    }
+
+    /**
+     * Builds a {@link Data#CONTENT_URI} for the current cursor
+     * position.
+     */
+    public Uri getDataUri() {
+        Cursor cursor = getCursor();
+        long id = cursor.getLong(POSTAL_ID_COLUMN_INDEX);
+        return ContentUris.withAppendedId(Data.CONTENT_URI, id);
+    }
+
+    @Override
+    public View newView(Context context, Cursor cursor, ViewGroup parent) {
+        final ContactListItemView view = new ContactListItemView(context, null);
+        view.setUnknownNameText(mUnknownNameText);
+        view.setTextWithHighlightingFactory(getTextWithHighlightingFactory());
+        return view;
+    }
+
+    @Override
+    public void bindView(View itemView, Context context, Cursor cursor) {
+        ContactListItemView view = (ContactListItemView)itemView;
+        bindSectionHeaderAndDivider(view, cursor);
+        bindName(view, cursor);
+        bindPhoto(view, cursor);
+        bindPostalAddress(view, cursor);
+    }
+
+    protected void bindPostalAddress(ContactListItemView view, Cursor cursor) {
+        CharSequence label = null;
+        if (!cursor.isNull(POSTAL_TYPE_COLUMN_INDEX)) {
+            final int type = cursor.getInt(POSTAL_TYPE_COLUMN_INDEX);
+            final String customLabel = cursor.getString(POSTAL_LABEL_COLUMN_INDEX);
+
+            // TODO cache
+            label = StructuredPostal.getTypeLabel(getContext().getResources(), type, label);
+        }
+        view.setLabel(label);
+        view.showData(cursor, POSTAL_ADDRESS_COLUMN_INDEX);
+    }
+
+    protected void bindSectionHeaderAndDivider(final ContactListItemView view, Cursor cursor) {
+        final int position = cursor.getPosition();
+        final int section = getSectionForPosition(position);
+        if (getPositionForSection(section) == position) {
+            String title = (String)getSections()[section];
+            view.setSectionHeader(title);
+        } else {
+            view.setDividerVisible(false);
+            view.setSectionHeader(null);
+        }
+
+        // move the divider for the last item in a section
+        if (getPositionForSection(section + 1) - 1 == position) {
+            view.setDividerVisible(false);
+        } else {
+            view.setDividerVisible(true);
+        }
+    }
+
+    protected void bindName(final ContactListItemView view, Cursor cursor) {
+        view.showDisplayName(cursor, mDisplayNameColumnIndex, isNameHighlightingEnabled(),
+                mAlternativeDisplayNameColumnIndex);
+//        view.showPhoneticName(cursor, PHONE_PHONETIC_NAME_COLUMN_INDEX);
+    }
+
+    protected void bindPhoto(final ContactListItemView view, Cursor cursor) {
+        long photoId = 0;
+        if (!cursor.isNull(POSTAL_PHOTO_ID_COLUMN_INDEX)) {
+            photoId = cursor.getLong(POSTAL_PHOTO_ID_COLUMN_INDEX);
+        }
+
+        getPhotoLoader().loadPhoto(view.getPhotoView(), photoId);
+    }
+//
+//    protected void bindSearchSnippet(final ContactListItemView view, Cursor cursor) {
+//        view.showSnippet(cursor, SUMMARY_SNIPPET_MIMETYPE_COLUMN_INDEX,
+//                SUMMARY_SNIPPET_DATA1_COLUMN_INDEX, SUMMARY_SNIPPET_DATA4_COLUMN_INDEX);
+//    }
+
+}
diff --git a/src/com/android/contacts/list/PostalAddressPickerFragment.java b/src/com/android/contacts/list/PostalAddressPickerFragment.java
new file mode 100644
index 0000000..734873b
--- /dev/null
+++ b/src/com/android/contacts/list/PostalAddressPickerFragment.java
@@ -0,0 +1,89 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+
+import android.net.Uri;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Fragment containing a postal address list for picking.
+ */
+public class PostalAddressPickerFragment
+        extends ContactEntryListFragment<ContactEntryListAdapter> {
+    private OnPostalAddressPickerActionListener mListener;
+
+    public PostalAddressPickerFragment() {
+        setPhotoLoaderEnabled(true);
+        setSectionHeaderDisplayEnabled(true);
+    }
+
+    public void setOnPostalAddressPickerActionListener(
+            OnPostalAddressPickerActionListener listener) {
+        this.mListener = listener;
+    }
+
+    @Override
+    protected void onItemClick(int position, long id) {
+        if (!isLegacyCompatibilityMode()) {
+            PostalAddressListAdapter adapter = (PostalAddressListAdapter)getAdapter();
+//          if (adapter.isSearchAllContactsItemPosition(position)) {
+//              searchAllContacts();
+//          } else {
+            adapter.moveToPosition(position);
+            pickPostalAddress(adapter.getDataUri());
+//          }
+        } else {
+            LegacyPostalAddressListAdapter adapter = (LegacyPostalAddressListAdapter)getAdapter();
+            adapter.moveToPosition(position);
+            pickPostalAddress(adapter.getContactMethodUri());
+        }
+    }
+
+    @Override
+    protected ContactEntryListAdapter createListAdapter() {
+        if (!isLegacyCompatibilityMode()) {
+            PostalAddressListAdapter adapter = new PostalAddressListAdapter(getActivity());
+            adapter.setSectionHeaderDisplayEnabled(true);
+            adapter.setDisplayPhotos(true);
+            return adapter;
+        } else {
+            LegacyPostalAddressListAdapter adapter =
+                    new LegacyPostalAddressListAdapter(getActivity());
+            adapter.setSectionHeaderDisplayEnabled(false);
+            adapter.setDisplayPhotos(false);
+            return adapter;
+        }
+    }
+
+    @Override
+    protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+        if (isSearchMode()) {
+            return inflater.inflate(R.layout.contacts_search_content, null);
+        } else if (isSearchResultsMode()) {
+            return inflater.inflate(R.layout.contacts_list_search_results, null);
+        } else {
+            return inflater.inflate(R.layout.contacts_list_content, null);
+        }
+    }
+
+    public void pickPostalAddress(Uri uri) {
+        mListener.onPickPostalAddressAction(uri);
+    }
+}
diff --git a/src/com/android/contacts/list/ProviderStatusLoader.java b/src/com/android/contacts/list/ProviderStatusLoader.java
new file mode 100644
index 0000000..c7bd547
--- /dev/null
+++ b/src/com/android/contacts/list/ProviderStatusLoader.java
@@ -0,0 +1,230 @@
+/*
+ * 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.list;
+
+import com.android.contacts.ContactsUtils;
+import com.android.contacts.PhoneDisambigDialog;
+import com.android.contacts.R;
+
+import android.app.patterns.CursorLoader;
+import android.app.patterns.LoaderManagingFragment;
+import android.content.AsyncQueryHandler;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.Settings;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.ProviderStatus;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts.Data;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.TextView;
+
+/**
+ * Checks provider status and configures a list adapter accordingly.
+ */
+public class ProviderStatusLoader {
+
+    private final CursorLoader mLoader;
+
+    public ProviderStatusLoader(CursorLoader loader) {
+        this.mLoader = loader;
+    }
+
+    public int getProviderStatus() {
+        // This query can be performed on the UI thread because
+        // the API explicitly allows such use.
+        Cursor cursor = mLoader.getContext().getContentResolver().query(
+                ProviderStatus.CONTENT_URI,
+                new String[] { ProviderStatus.STATUS, ProviderStatus.DATA1 }, null, null, null);
+        if (cursor != null) {
+            try {
+                if (cursor.moveToFirst()) {
+                    return cursor.getInt(0);
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+
+        return ProviderStatus.STATUS_NORMAL;
+    }
+
+
+//  View importFailureView = findViewById(R.id.import_failure);
+//  if (importFailureView == null) {
+//      return true;
+//  }
+//
+//  TextView messageView = (TextView) findViewById(R.id.emptyText);
+//
+//  // This query can be performed on the UI thread because
+//  // the API explicitly allows such use.
+//  Cursor cursor = getContentResolver().query(ProviderStatus.CONTENT_URI,
+//          new String[] { ProviderStatus.STATUS, ProviderStatus.DATA1 }, null, null, null);
+//  if (cursor != null) {
+//      try {
+//          if (cursor.moveToFirst()) {
+//              int status = cursor.getInt(0);
+//              if (status != mProviderStatus) {
+//                  mProviderStatus = status;
+//                  switch (status) {
+//                      case ProviderStatus.STATUS_NORMAL:
+//                          mAdapter.notifyDataSetInvalidated();
+//                          if (loadData) {
+//                              startQuery();
+//                          }
+//                          break;
+//
+//                      case ProviderStatus.STATUS_CHANGING_LOCALE:
+//                          messageView.setText(R.string.locale_change_in_progress);
+//                          mAdapter.changeCursor(null);
+//                          mAdapter.notifyDataSetInvalidated();
+//                          break;
+//
+//                      case ProviderStatus.STATUS_UPGRADING:
+//                          messageView.setText(R.string.upgrade_in_progress);
+//                          mAdapter.changeCursor(null);
+//                          mAdapter.notifyDataSetInvalidated();
+//                          break;
+//
+//                      case ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY:
+//                          long size = cursor.getLong(1);
+//                          String message = getResources().getString(
+//                                  R.string.upgrade_out_of_memory, new Object[] {size});
+//                          messageView.setText(message);
+//                          configureImportFailureView(importFailureView);
+//                          mAdapter.changeCursor(null);
+//                          mAdapter.notifyDataSetInvalidated();
+//                          break;
+//                  }
+//              }
+//          }
+//      } finally {
+//          cursor.close();
+//      }
+//  }
+//
+//  importFailureView.setVisibility(
+//          mProviderStatus == ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY
+//                  ? View.VISIBLE
+//                  : View.GONE);
+//  return mProviderStatus == ProviderStatus.STATUS_NORMAL;
+//}
+
+//
+//    /**
+//     * Obtains the contacts provider status and configures the UI accordingly.
+//     *
+//     * @param loadData true if the method needs to start a query when the
+//     *            provider is in the normal state
+//     * @return true if the provider status is normal
+//     */
+//    private boolean checkProviderState(boolean loadData) {
+//        View importFailureView = findViewById(R.id.import_failure);
+//        if (importFailureView == null) {
+//            return true;
+//        }
+//
+//        TextView messageView = (TextView) findViewById(R.id.emptyText);
+//
+//        // This query can be performed on the UI thread because
+//        // the API explicitly allows such use.
+//        Cursor cursor = getContentResolver().query(ProviderStatus.CONTENT_URI,
+//                new String[] { ProviderStatus.STATUS, ProviderStatus.DATA1 }, null, null, null);
+//        if (cursor != null) {
+//            try {
+//                if (cursor.moveToFirst()) {
+//                    int status = cursor.getInt(0);
+//                    if (status != mProviderStatus) {
+//                        mProviderStatus = status;
+//                        switch (status) {
+//                            case ProviderStatus.STATUS_NORMAL:
+//                                mAdapter.notifyDataSetInvalidated();
+//                                if (loadData) {
+//                                    startQuery();
+//                                }
+//                                break;
+//
+//                            case ProviderStatus.STATUS_CHANGING_LOCALE:
+//                                messageView.setText(R.string.locale_change_in_progress);
+//                                mAdapter.changeCursor(null);
+//                                mAdapter.notifyDataSetInvalidated();
+//                                break;
+//
+//                            case ProviderStatus.STATUS_UPGRADING:
+//                                messageView.setText(R.string.upgrade_in_progress);
+//                                mAdapter.changeCursor(null);
+//                                mAdapter.notifyDataSetInvalidated();
+//                                break;
+//
+//                            case ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY:
+//                                long size = cursor.getLong(1);
+//                                String message = getResources().getString(
+//                                        R.string.upgrade_out_of_memory, new Object[] {size});
+//                                messageView.setText(message);
+//                                configureImportFailureView(importFailureView);
+//                                mAdapter.changeCursor(null);
+//                                mAdapter.notifyDataSetInvalidated();
+//                                break;
+//                        }
+//                    }
+//                }
+//            } finally {
+//                cursor.close();
+//            }
+//        }
+//
+//        importFailureView.setVisibility(
+//                mProviderStatus == ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY
+//                        ? View.VISIBLE
+//                        : View.GONE);
+//        return mProviderStatus == ProviderStatus.STATUS_NORMAL;
+//    }
+//
+//    private void configureImportFailureView(View importFailureView) {
+//
+//        OnClickListener listener = new OnClickListener(){
+//
+//            public void onClick(View v) {
+//                switch(v.getId()) {
+//                    case R.id.import_failure_uninstall_apps: {
+//                        startActivity(new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS));
+//                        break;
+//                    }
+//                    case R.id.import_failure_retry_upgrade: {
+//                        // Send a provider status update, which will trigger a retry
+//                        ContentValues values = new ContentValues();
+//                        values.put(ProviderStatus.STATUS, ProviderStatus.STATUS_UPGRADING);
+//                        getContentResolver().update(ProviderStatus.CONTENT_URI, values, null, null);
+//                        break;
+//                    }
+//                }
+//            }};
+//
+//        Button uninstallApps = (Button) findViewById(R.id.import_failure_uninstall_apps);
+//        uninstallApps.setOnClickListener(listener);
+//
+//        Button retryUpgrade = (Button) findViewById(R.id.import_failure_retry_upgrade);
+//        retryUpgrade.setOnClickListener(listener);
+//    }
+
+}
diff --git a/src/com/android/contacts/list/ShortcutIntentBuilder.java b/src/com/android/contacts/list/ShortcutIntentBuilder.java
new file mode 100644
index 0000000..7a4f9de
--- /dev/null
+++ b/src/com/android/contacts/list/ShortcutIntentBuilder.java
@@ -0,0 +1,397 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+import com.android.contacts.util.Constants;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+
+import java.util.Random;
+
+/**
+ * Constructs shortcut intents.
+ */
+public class ShortcutIntentBuilder {
+
+    private static final String[] CONTACT_COLUMNS = {
+        Contacts.DISPLAY_NAME,
+        Contacts.PHOTO_ID,
+    };
+
+    private static final int CONTACT_DISPLAY_NAME_COLUMN_INDEX = 0;
+    private static final int CONTACT_PHOTO_ID_COLUMN_INDEX = 1;
+
+    private static final String[] PHONE_COLUMNS = {
+        Phone.DISPLAY_NAME,
+        Phone.PHOTO_ID,
+        Phone.NUMBER,
+        Phone.TYPE,
+    };
+
+    private static final int PHONE_DISPLAY_NAME_COLUMN_INDEX = 0;
+    private static final int PHONE_PHOTO_ID_COLUMN_INDEX = 1;
+    private static final int PHONE_NUMBER_COLUMN_INDEX = 2;
+    private static final int PHONE_TYPE_COLUMN_INDEX = 3;
+
+    private static final String[] PHOTO_COLUMNS = {
+        Photo.PHOTO,
+    };
+
+    private static final int PHOTO_PHOTO_COLUMN_INDEX = 0;
+
+    private static final String PHOTO_SELECTION = Photo._ID + "=?";
+
+    private final OnShortcutIntentCreatedListener mListener;
+    private final Context mContext;
+    private final int mIconSize;
+
+    /**
+     * Listener interface.
+     */
+    public interface OnShortcutIntentCreatedListener {
+
+        /**
+         * Callback for shortcut intent creation.
+         *
+         * @param uri the original URI for which the shortcut intent has been
+         *            created.
+         * @param shortcutIntent resulting shortcut intent.
+         */
+        void onShortcutIntentCreated(Uri uri, Intent shortcutIntent);
+    }
+
+    public ShortcutIntentBuilder(Context context, OnShortcutIntentCreatedListener listener) {
+        mContext = context;
+        mListener = listener;
+
+        mIconSize = context.getResources().getDimensionPixelSize(android.R.dimen.app_icon_size);
+    }
+
+    public void createContactShortcutIntent(Uri contactUri) {
+        new ContactLoadingAsyncTask(contactUri).execute();
+    }
+
+    public void createPhoneNumberShortcutIntent(Uri dataUri, String shortcutAction) {
+        new PhoneNumberLoadingAsyncTask(dataUri, shortcutAction).execute();
+    }
+
+    /**
+     * An asynchronous task that loads name, photo and other data from the database.
+     */
+    private abstract class LoadingAsyncTask extends AsyncTask<Void, Void, Void> {
+        protected Uri mUri;
+        protected String mDisplayName;
+        protected byte[] mBitmapData;
+        protected long mPhotoId;
+
+        public LoadingAsyncTask(Uri uri) {
+            mUri = uri;
+        }
+
+        @Override
+        protected Void doInBackground(Void... params) {
+            loadData();
+            loadPhoto();
+            return null;
+        }
+
+        protected abstract void loadData();
+
+        private void loadPhoto() {
+            if (mPhotoId == 0) {
+                return;
+            }
+
+            ContentResolver resolver = mContext.getContentResolver();
+            Cursor cursor = resolver.query(Data.CONTENT_URI, PHOTO_COLUMNS, PHOTO_SELECTION,
+                    new String[] { String.valueOf(mPhotoId) }, null);
+            if (cursor != null) {
+                try {
+                    if (cursor.moveToFirst()) {
+                        mBitmapData = cursor.getBlob(PHOTO_PHOTO_COLUMN_INDEX);
+                    }
+                } finally {
+                    cursor.close();
+                }
+            }
+        }
+    }
+
+    private final class ContactLoadingAsyncTask extends LoadingAsyncTask {
+        public ContactLoadingAsyncTask(Uri uri) {
+            super(uri);
+        }
+
+        @Override
+        protected void loadData() {
+            ContentResolver resolver = mContext.getContentResolver();
+            Cursor cursor = resolver.query(mUri, CONTACT_COLUMNS, null, null, null);
+            if (cursor != null) {
+                try {
+                    if (cursor.moveToFirst()) {
+                        mDisplayName = cursor.getString(CONTACT_DISPLAY_NAME_COLUMN_INDEX);
+                        mPhotoId = cursor.getLong(CONTACT_PHOTO_ID_COLUMN_INDEX);
+                    }
+                } finally {
+                    cursor.close();
+                }
+            }
+        }
+        @Override
+        protected void onPostExecute(Void result) {
+            createContactShortcutIntent(mUri, mDisplayName, mBitmapData);
+        }
+    }
+
+    private final class PhoneNumberLoadingAsyncTask extends LoadingAsyncTask {
+        private final String mShortcutAction;
+        private String mPhoneNumber;
+        private int mPhoneType;
+
+        public PhoneNumberLoadingAsyncTask(Uri uri, String shortcutAction) {
+            super(uri);
+            mShortcutAction = shortcutAction;
+        }
+
+        @Override
+        protected void loadData() {
+            ContentResolver resolver = mContext.getContentResolver();
+            Cursor cursor = resolver.query(mUri, PHONE_COLUMNS, null, null, null);
+            if (cursor != null) {
+                try {
+                    if (cursor.moveToFirst()) {
+                        mDisplayName = cursor.getString(PHONE_DISPLAY_NAME_COLUMN_INDEX);
+                        mPhotoId = cursor.getLong(PHONE_PHOTO_ID_COLUMN_INDEX);
+                        mPhoneNumber = cursor.getString(PHONE_NUMBER_COLUMN_INDEX);
+                        mPhoneType = cursor.getInt(PHONE_TYPE_COLUMN_INDEX);
+                    }
+                } finally {
+                    cursor.close();
+                }
+            }
+        }
+
+        @Override
+        protected void onPostExecute(Void result) {
+            createPhoneNumberShortcutIntent(mUri, mDisplayName, mBitmapData, mPhoneNumber,
+                    mPhoneType, mShortcutAction);
+        }
+    }
+
+    private void createContactShortcutIntent(Uri contactUri, String displayName,
+            byte[] bitmapData) {
+        Bitmap bitmap;
+        if (bitmapData != null) {
+            bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length, null);
+        } else {
+            final int[] fallbacks = {
+                R.drawable.ic_contact_picture,
+                R.drawable.ic_contact_picture_2,
+                R.drawable.ic_contact_picture_3
+            };
+            bitmap = BitmapFactory.decodeResource(mContext.getResources(),
+                    fallbacks[new Random().nextInt(fallbacks.length)]);
+        }
+
+        Intent shortcutIntent;
+        // This is a simple shortcut to view a contact.
+        shortcutIntent = new Intent(ContactsContract.QuickContact.ACTION_QUICK_CONTACT);
+        shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
+                Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+
+        shortcutIntent.setData(contactUri);
+        shortcutIntent.putExtra(ContactsContract.QuickContact.EXTRA_MODE,
+                ContactsContract.QuickContact.MODE_LARGE);
+        shortcutIntent.putExtra(ContactsContract.QuickContact.EXTRA_EXCLUDE_MIMES,
+                (String[]) null);
+        shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+        final Bitmap icon = scaleToAppIconSize(framePhoto(bitmap));
+
+        Intent intent = new Intent();
+        intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon);
+        intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
+        intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, displayName);
+
+        mListener.onShortcutIntentCreated(contactUri, intent);
+    }
+
+    private void createPhoneNumberShortcutIntent(Uri uri, String displayName, byte[] bitmapData,
+            String phoneNumber, int phoneType, String shortcutAction) {
+        Bitmap bitmap = null;
+        if (bitmapData != null) {
+            bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length, null);
+        }
+
+        Uri phoneUri;
+        if (Intent.ACTION_CALL.equals(shortcutAction)) {
+            // Make the URI a direct tel: URI so that it will always continue to work
+            phoneUri = Uri.fromParts(Constants.SCHEME_TEL, phoneNumber, null);
+            bitmap = generatePhoneNumberIcon(bitmap, phoneType, R.drawable.badge_action_call);
+        } else {
+            phoneUri = Uri.fromParts(Constants.SCHEME_SMSTO, phoneNumber, null);
+            bitmap = generatePhoneNumberIcon(bitmap, phoneType, R.drawable.badge_action_sms);
+        }
+
+        Intent shortcutIntent = new Intent(shortcutAction, phoneUri);
+        shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+        Intent intent = new Intent();
+        intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, bitmap);
+        intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
+        intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, displayName);
+
+        mListener.onShortcutIntentCreated(uri, intent);
+    }
+
+    private Bitmap framePhoto(Bitmap photo) {
+        final Resources r = mContext.getResources();
+        final Drawable frame = r.getDrawable(com.android.internal.R.drawable.quickcontact_badge);
+
+        final int width = r.getDimensionPixelSize(R.dimen.contact_shortcut_frame_width);
+        final int height = r.getDimensionPixelSize(R.dimen.contact_shortcut_frame_height);
+
+        frame.setBounds(0, 0, width, height);
+
+        final Rect padding = new Rect();
+        frame.getPadding(padding);
+
+        final Rect source = new Rect(0, 0, photo.getWidth(), photo.getHeight());
+        final Rect destination = new Rect(padding.left, padding.top,
+                width - padding.right, height - padding.bottom);
+
+        final int d = Math.max(width, height);
+        final Bitmap b = Bitmap.createBitmap(d, d, Bitmap.Config.ARGB_8888);
+        final Canvas c = new Canvas(b);
+
+        c.translate((d - width) / 2.0f, (d - height) / 2.0f);
+        frame.draw(c);
+        c.drawBitmap(photo, source, destination, new Paint(Paint.FILTER_BITMAP_FLAG));
+
+        return b;
+    }
+
+    private Bitmap scaleToAppIconSize(Bitmap photo) {
+
+        // Setup the drawing classes
+        Bitmap icon = Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(icon);
+
+        // Copy in the photo
+        Paint photoPaint = new Paint();
+        photoPaint.setDither(true);
+        photoPaint.setFilterBitmap(true);
+        Rect src = new Rect(0,0, photo.getWidth(),photo.getHeight());
+        Rect dst = new Rect(0,0, mIconSize, mIconSize);
+        canvas.drawBitmap(photo, src, dst, photoPaint);
+
+        return icon;
+    }
+
+    /**
+     * Generates a phone number shortcut icon. Adds an overlay describing the type of the phone
+     * number, and if there is a photo also adds the call action icon.
+     */
+    private Bitmap generatePhoneNumberIcon(Bitmap photo, int phoneType, int actionResId) {
+        final Resources r = mContext.getResources();
+        boolean drawPhoneOverlay = true;
+        final float scaleDensity = r.getDisplayMetrics().scaledDensity;
+
+        Bitmap phoneIcon = ((BitmapDrawable) r.getDrawable(actionResId)).getBitmap();
+
+        // If there isn't a photo use the generic phone action icon instead
+        if (photo == null) {
+            photo = phoneIcon;
+            drawPhoneOverlay = false;
+        }
+
+        // Setup the drawing classes
+        Bitmap icon = Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(icon);
+
+        // Copy in the photo
+        Paint photoPaint = new Paint();
+        photoPaint.setDither(true);
+        photoPaint.setFilterBitmap(true);
+        Rect src = new Rect(0, 0, photo.getWidth(), photo.getHeight());
+        Rect dst = new Rect(0, 0, mIconSize, mIconSize);
+        canvas.drawBitmap(photo, src, dst, photoPaint);
+
+        // Create an overlay for the phone number type
+        String overlay = null;
+        switch (phoneType) {
+            case Phone.TYPE_HOME:
+                overlay = mContext.getString(R.string.type_short_home);
+                break;
+
+            case Phone.TYPE_MOBILE:
+                overlay = mContext.getString(R.string.type_short_mobile);
+                break;
+
+            case Phone.TYPE_WORK:
+                overlay = mContext.getString(R.string.type_short_work);
+                break;
+
+            case Phone.TYPE_PAGER:
+                overlay = mContext.getString(R.string.type_short_pager);
+                break;
+
+            case Phone.TYPE_OTHER:
+                overlay = mContext.getString(R.string.type_short_other);
+                break;
+        }
+
+        if (overlay != null) {
+            Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG);
+            textPaint.setTextSize(20.0f * scaleDensity);
+            textPaint.setTypeface(Typeface.DEFAULT_BOLD);
+            textPaint.setColor(r.getColor(R.color.textColorIconOverlay));
+            textPaint.setShadowLayer(3f, 1, 1, r.getColor(R.color.textColorIconOverlayShadow));
+            canvas.drawText(overlay, 2 * scaleDensity, 16 * scaleDensity, textPaint);
+        }
+
+        // Draw the phone action icon as an overlay
+        if (drawPhoneOverlay) {
+            src.set(0, 0, phoneIcon.getWidth(), phoneIcon.getHeight());
+            int iconWidth = icon.getWidth();
+            dst.set(iconWidth - ((int) (20 * scaleDensity)), -1,
+                    iconWidth, ((int) (19 * scaleDensity)));
+            canvas.drawBitmap(phoneIcon, src, dst, photoPaint);
+        }
+
+        return icon;
+    }
+}
diff --git a/src/com/android/contacts/list/StrequentContactListAdapter.java b/src/com/android/contacts/list/StrequentContactListAdapter.java
new file mode 100644
index 0000000..1fa4077
--- /dev/null
+++ b/src/com/android/contacts/list/StrequentContactListAdapter.java
@@ -0,0 +1,199 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+
+import android.app.patterns.CursorLoader;
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+import android.widget.ListView;
+import android.widget.TextView;
+
+/**
+ * A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type loading
+ * a combination of starred and frequently contacted.
+ */
+public class StrequentContactListAdapter extends ContactListAdapter {
+
+    private int mFrequentSeparatorPos;
+    private TextView mSeparatorView;
+    private OnClickListener mCallButtonListener;
+    private int mCallButtonId;
+    private boolean mStarredContactsIncluded;
+    private boolean mFrequentlyContactedContactsIncluded;
+
+    public StrequentContactListAdapter(Context context, int callButtonId) {
+        super(context);
+        mCallButtonId = callButtonId;
+    }
+
+    public void setCallButtonListener(OnClickListener callButtonListener) {
+        mCallButtonListener = callButtonListener;
+    }
+
+    public void setStarredContactsIncluded(boolean flag) {
+        mStarredContactsIncluded = flag;
+    }
+
+    public void setFrequentlyContactedContactsIncluded(boolean flag) {
+        mFrequentlyContactedContactsIncluded = flag;
+    }
+
+    @Override
+    public void configureLoader(CursorLoader loader) {
+        String sortOrder = getSortOrder() == ContactsContract.Preferences.SORT_ORDER_PRIMARY
+                ? Contacts.SORT_KEY_PRIMARY
+                : Contacts.SORT_KEY_ALTERNATIVE;
+        if (mStarredContactsIncluded && mFrequentlyContactedContactsIncluded) {
+            loader.setUri(Contacts.CONTENT_STREQUENT_URI);
+        } else if (mStarredContactsIncluded) {
+            loader.setUri(Contacts.CONTENT_URI);
+            loader.setSelection(Contacts.STARRED + "!=0");
+        } else if (mFrequentlyContactedContactsIncluded) {
+            loader.setUri(Contacts.CONTENT_URI);
+            loader.setSelection(Contacts.TIMES_CONTACTED + " > 0");
+            sortOrder = Contacts.TIMES_CONTACTED + " DESC";
+        } else {
+            throw new UnsupportedOperationException("Neither StarredContactsIncluded nor "
+                    + "FrequentlyContactedContactsIncluded is set");
+        }
+
+        loader.setProjection(PROJECTION);
+        loader.setSortOrder(sortOrder);
+    }
+
+    @Override
+    public void changeCursor(Cursor cursor) {
+        super.changeCursor(cursor);
+
+        // Get the split between starred and frequent items, if the mode is strequent
+        mFrequentSeparatorPos = ListView.INVALID_POSITION;
+
+        if (mStarredContactsIncluded && mFrequentlyContactedContactsIncluded) {
+            int count = 0;
+            if (cursor != null && (count = cursor.getCount()) > 0) {
+                cursor.moveToPosition(-1);
+                for (int i = 0; cursor.moveToNext(); i++) {
+                    int starred = cursor.getInt(CONTACT_STARRED_COLUMN_INDEX);
+                    if (starred == 0) {
+                        if (i > 0) {
+                            // Only add the separator when there are starred items present
+                            mFrequentSeparatorPos = i;
+                        }
+                        break;
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    public int getCount() {
+        if (mFrequentSeparatorPos == ListView.INVALID_POSITION) {
+            return super.getCount();
+        } else {
+            // Add a row for the separator
+            return super.getCount() + 1;
+        }
+    }
+
+    @Override
+    public boolean areAllItemsEnabled() {
+        return mFrequentSeparatorPos == ListView.INVALID_POSITION;
+    }
+
+    @Override
+    public boolean isEnabled(int position) {
+        return position != mFrequentSeparatorPos;
+    }
+
+    @Override
+    public Object getItem(int position) {
+        if (mFrequentSeparatorPos == ListView.INVALID_POSITION
+                || position < mFrequentSeparatorPos) {
+            return super.getItem(position);
+        } else {
+            return super.getItem(position - 1);
+        }
+    }
+
+    @Override
+    public long getItemId(int position) {
+        if (mFrequentSeparatorPos == ListView.INVALID_POSITION
+                || position < mFrequentSeparatorPos) {
+            return super.getItemId(position);
+        } else {
+            return super.getItemId(position - 1);
+        }
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        if (position == mFrequentSeparatorPos) {
+            return IGNORE_ITEM_VIEW_TYPE;
+        }
+
+        return super.getItemViewType(position);
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        if (mFrequentSeparatorPos == ListView.INVALID_POSITION
+                || position < mFrequentSeparatorPos) {
+            return super.getView(position, convertView, parent);
+        } else if (position == mFrequentSeparatorPos) {
+            if (mSeparatorView == null) {
+                mSeparatorView = (TextView)LayoutInflater.from(getContext()).
+                        inflate(R.layout.list_separator, parent, false);
+                mSeparatorView.setText(R.string.favoritesFrquentSeparator);
+            }
+            return mSeparatorView;
+        } else {
+            return super.getView(position - 1, convertView, parent);
+        }
+    }
+
+    @Override
+    public View newView(Context context, Cursor cursor, ViewGroup parent) {
+        ContactListItemView view = (ContactListItemView)super.newView(context, cursor, parent);
+        view.setOnCallButtonClickListener(mCallButtonListener);
+        return view;
+    }
+
+    @Override
+    public void bindView(View itemView, Context context, Cursor cursor) {
+        final ContactListItemView view = (ContactListItemView)itemView;
+
+        bindName(view, cursor);
+        bindQuickContact(view, cursor);
+        bindPresence(view, cursor);
+
+        // Make the call button visible if requested.
+        if (getHasPhoneNumber()) {
+            int position = cursor.getPosition();
+            view.showCallButton(mCallButtonId, position);
+        } else {
+            view.hideCallButton();
+        }
+    }
+}
diff --git a/src/com/android/contacts/list/StrequentContactListFragment.java b/src/com/android/contacts/list/StrequentContactListFragment.java
new file mode 100644
index 0000000..9779d10
--- /dev/null
+++ b/src/com/android/contacts/list/StrequentContactListFragment.java
@@ -0,0 +1,103 @@
+/*
+ * 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.list;
+
+import com.android.contacts.R;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+
+/**
+ * Fragment containing a list of starred contacts followed by a list of frequently contacted.
+ */
+public class StrequentContactListFragment extends ContactBrowseListFragment
+        implements OnClickListener {
+
+    private static final int CALL_BUTTON_ID = android.R.id.button1;
+
+    private boolean mStarredContactsIncluded = true;
+    private boolean mFrequentlyContactedContactsIncluded = true;
+
+    public StrequentContactListFragment() {
+        setSectionHeaderDisplayEnabled(false);
+        setPhotoLoaderEnabled(true);
+    }
+
+    public void setStarredContactsIncluded(boolean flag) {
+        mStarredContactsIncluded = flag;
+        configureAdapter();
+    }
+
+    public void setFrequentlyContactedContactsIncluded(boolean flag) {
+        mFrequentlyContactedContactsIncluded = flag;
+        configureAdapter();
+    }
+
+    @Override
+    protected void onItemClick(int position, long id) {
+        ContactListAdapter adapter = getAdapter();
+        adapter.moveToPosition(position);
+        viewContact(adapter.getContactUri());
+    }
+
+    @Override
+    protected ContactListAdapter createListAdapter() {
+        StrequentContactListAdapter adapter =
+                new StrequentContactListAdapter(getActivity(), CALL_BUTTON_ID);
+        adapter.setSectionHeaderDisplayEnabled(false);
+        adapter.setDisplayPhotos(true);
+        adapter.setQuickContactEnabled(true);
+        adapter.setCallButtonListener(this);
+
+        return adapter;
+    }
+
+    @Override
+    protected void configureAdapter() {
+        super.configureAdapter();
+
+        StrequentContactListAdapter adapter = (StrequentContactListAdapter)getAdapter();
+        if (adapter != null) {
+            adapter.setStarredContactsIncluded(mStarredContactsIncluded);
+            adapter.setFrequentlyContactedContactsIncluded(mFrequentlyContactedContactsIncluded);
+        }
+    }
+
+    @Override
+    protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+        return inflater.inflate(R.layout.contacts_list_content, null);
+    }
+
+    @Override
+    protected void prepareEmptyView() {
+        setEmptyText(R.string.noFavoritesHelpText);
+    }
+
+    public void onClick(View v) {
+        int id = v.getId();
+        switch (id) {
+            case CALL_BUTTON_ID: {
+                final int position = (Integer)v.getTag();
+                ContactListAdapter adapter = getAdapter();
+                adapter.moveToPosition(position);
+                callContact(adapter.getContactUri());
+                break;
+            }
+        }
+    }
+}
diff --git a/src/com/android/contacts/model/GoogleSource.java b/src/com/android/contacts/model/GoogleSource.java
index 90abc92..8786fcb 100644
--- a/src/com/android/contacts/model/GoogleSource.java
+++ b/src/com/android/contacts/model/GoogleSource.java
@@ -157,113 +157,6 @@
         return super.inflateWebsite(context, inflateLevel);
     }
 
-    // TODO: this should come from resource in the future
-    // Note that frameworks/base/core/java/android/pim/vcard/VCardEntry.java also wants
-    // this String.
-    private static final String GOOGLE_MY_CONTACTS_GROUP = "System Group: My Contacts";
-
-    public static final void attemptMyContactsMembership(EntityDelta state, Context context) {
-        final ValuesDelta stateValues = state.getValues();
-	stateValues.setFromTemplate(true);
-        final String accountName = stateValues.getAsString(RawContacts.ACCOUNT_NAME);
-        final String accountType = stateValues.getAsString(RawContacts.ACCOUNT_TYPE);
-        attemptMyContactsMembership(state, accountName, accountType, context, true);
-    }
-
-    public static final void createMyContactsIfNotExist(Account account, Context context) {
-        attemptMyContactsMembership(null, account.name, account.type, context, true);
-    }
-
-    /**
-     *
-     * @param allowRecur If the group is created between querying/about to create, we recur.  But
-     *     to prevent excess recursion, we provide a flag to make sure we only do the recursion loop
-     *     once
-     */
-    private static final void attemptMyContactsMembership(EntityDelta state,
-                final String accountName, final String accountType, Context context,
-                boolean allowRecur) {
-        final ContentResolver resolver = context.getContentResolver();
-
-        Cursor cursor = resolver.query(Groups.CONTENT_URI,
-                new String[] {Groups.TITLE, Groups.SOURCE_ID, Groups.SHOULD_SYNC},
-                Groups.ACCOUNT_NAME + " =? AND " + Groups.ACCOUNT_TYPE + " =?",
-                new String[] {accountName, accountType}, null);
-
-        boolean myContactsExists = false;
-        long assignToGroupSourceId = -1;
-        while (cursor.moveToNext()) {
-            if (GOOGLE_MY_CONTACTS_GROUP.equals(cursor.getString(0))) {
-                myContactsExists = true;
-            }
-            if (assignToGroupSourceId == -1 && cursor.getInt(2) != 0) {
-                assignToGroupSourceId = cursor.getInt(1);
-            }
-
-            if (myContactsExists && assignToGroupSourceId != -1) {
-                break;
-            }
-        }
-
-        if (myContactsExists && state == null) {
-            return;
-        }
-
-        try {
-            final ContentValues values = new ContentValues();
-            values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
-
-            if (!myContactsExists) {
-                // create the group if it doesn't exist
-                final ContentValues newGroup = new ContentValues();
-                newGroup.put(Groups.TITLE, GOOGLE_MY_CONTACTS_GROUP);
-
-                newGroup.put(Groups.ACCOUNT_NAME, accountName);
-                newGroup.put(Groups.ACCOUNT_TYPE, accountType);
-                newGroup.put(Groups.GROUP_VISIBLE, "1");
-
-                ArrayList<ContentProviderOperation> operations =
-                    new ArrayList<ContentProviderOperation>();
-
-                operations.add(ContentProviderOperation
-                        .newAssertQuery(Groups.CONTENT_URI)
-                        .withSelection(SELECTION_GROUPS_BY_TITLE_AND_ACCOUNT,
-                                new String[] {GOOGLE_MY_CONTACTS_GROUP, accountName, accountType})
-                        .withExpectedCount(0).build());
-                operations.add(ContentProviderOperation
-                        .newInsert(Groups.CONTENT_URI)
-                        .withValues(newGroup)
-                        .build());
-                try {
-                    ContentProviderResult[] results = resolver.applyBatch(
-                            ContactsContract.AUTHORITY, operations);
-                    values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(results[1].uri));
-                } catch (RemoteException e) {
-                    throw new IllegalStateException("Problem querying for groups", e);
-                } catch (OperationApplicationException e) {
-                    // the group was created after the query but before we tried to create it
-                    if (allowRecur) {
-                        attemptMyContactsMembership(
-                                state, accountName, accountType, context, false);
-                    }
-                    return;
-                }
-            } else {
-                if (assignToGroupSourceId != -1) {
-                    values.put(GroupMembership.GROUP_SOURCE_ID, assignToGroupSourceId);
-                } else {
-                    // there are no Groups to add this contact to, so don't apply any membership
-                    // TODO: alert user that their contact will be dropped?
-                }
-            }
-            if (state != null) {
-                state.addEntry(ValuesDelta.fromAfter(values));
-            }
-        } finally {
-            cursor.close();
-        }
-    }
-
     @Override
     public int getHeaderColor(Context context) {
         return 0xff89c2c2;
diff --git a/src/com/android/contacts/ui/ContactsPreferencesActivity.java b/src/com/android/contacts/ui/ContactsPreferencesActivity.java
index 5a89745..8e0d94e 100644
--- a/src/com/android/contacts/ui/ContactsPreferencesActivity.java
+++ b/src/com/android/contacts/ui/ContactsPreferencesActivity.java
@@ -23,6 +23,7 @@
 import com.android.contacts.model.Sources;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
 import com.android.contacts.util.EmptyService;
+import com.android.contacts.util.LocalizedNameResolver;
 import com.android.contacts.util.WeakAsyncTask;
 import com.google.android.collect.Lists;
 
@@ -441,8 +442,17 @@
             put(mUngrouped ? Settings.UNGROUPED_VISIBLE : Groups.GROUP_VISIBLE, visible ? 1 : 0);
         }
 
+        private String getAccountType() {
+            return (mBefore == null ? mAfter : mBefore).getAsString(Settings.ACCOUNT_TYPE);
+        }
+
         public CharSequence getTitle(Context context) {
             if (mUngrouped) {
+                final String customAllContactsName =
+                        LocalizedNameResolver.getAllContactsName(context, getAccountType());
+                if (customAllContactsName != null) {
+                    return customAllContactsName;
+                }
                 if (mAccountHasGroups) {
                     return context.getText(R.string.display_ungrouped);
                 } else {
diff --git a/src/com/android/contacts/ui/EditContactActivity.java b/src/com/android/contacts/ui/EditContactActivity.java
index c70cff6..7b61e19 100644
--- a/src/com/android/contacts/ui/EditContactActivity.java
+++ b/src/com/android/contacts/ui/EditContactActivity.java
@@ -16,9 +16,9 @@
 
 package com.android.contacts.ui;
 
-import com.android.contacts.ContactsListActivity;
 import com.android.contacts.ContactsSearchManager;
 import com.android.contacts.ContactsUtils;
+import com.android.contacts.JoinContactActivity;
 import com.android.contacts.R;
 import com.android.contacts.model.ContactsSource;
 import com.android.contacts.model.Editor;
@@ -32,9 +32,9 @@
 import com.android.contacts.model.EntityDelta.ValuesDelta;
 import com.android.contacts.ui.widget.BaseContactEditorView;
 import com.android.contacts.ui.widget.PhotoEditorView;
+import com.android.contacts.util.DialogManager;
 import com.android.contacts.util.EmptyService;
 import com.android.contacts.util.WeakAsyncTask;
-import com.google.android.collect.Lists;
 
 import android.accounts.Account;
 import android.app.Activity;
@@ -53,6 +53,7 @@
 import android.content.Intent;
 import android.content.OperationApplicationException;
 import android.content.ContentProviderOperation.Builder;
+import android.content.DialogInterface.OnDismissListener;
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.media.MediaScannerConnection;
@@ -94,7 +95,8 @@
  * Activity for editing or inserting a contact.
  */
 public final class EditContactActivity extends Activity
-        implements View.OnClickListener, Comparator<EntityDelta> {
+        implements View.OnClickListener, Comparator<EntityDelta>,
+        DialogManager.DialogShowingViewActivity {
 
     private static final String TAG = "EditContactActivity";
 
@@ -127,6 +129,13 @@
     private static final int DIALOG_CONFIRM_READONLY_DELETE = 2;
     private static final int DIALOG_CONFIRM_MULTIPLE_DELETE = 3;
     private static final int DIALOG_CONFIRM_READONLY_HIDE = 4;
+    private static final int DIALOG_PICK_PHOTO = 5;
+    private static final int DIALOG_SPLIT = 6;
+    private static final int DIALOG_SELECT_ACCOUNT = 7;
+    private static final int DIALOG_VIEW_DIALOGS_ID1 = 8;
+    private static final int DIALOG_VIEW_DIALOGS_ID2 = 9;
+
+    private static final String BUNDLE_SELECT_ACCOUNT_LIST = "account_list";
 
     private static final int ICON_SIZE = 96;
 
@@ -144,14 +153,13 @@
     private static final int STATUS_SAVING = 2;
 
     private int mStatus;
+    private DialogManager mDialogManager;
 
     EntitySet mState;
 
     /** The linear layout holding the ContactEditorViews */
     LinearLayout mContent;
 
-    private ArrayList<Dialog> mManagedDialogs = Lists.newArrayList();
-
     private ViewIdGenerator mViewIdGenerator;
 
     @Override
@@ -163,6 +171,8 @@
 
         setContentView(R.layout.act_edit);
 
+        mDialogManager = new DialogManager(this, DIALOG_VIEW_DIALOGS_ID1, DIALOG_VIEW_DIALOGS_ID2);
+
         // Build editor and listen for photo requests
         mContent = (LinearLayout) findViewById(R.id.editors);
 
@@ -295,15 +305,6 @@
     }
 
     @Override
-    protected void onDestroy() {
-        super.onDestroy();
-
-        for (Dialog dialog : mManagedDialogs) {
-            dismissDialog(dialog);
-        }
-    }
-
-    @Override
     protected Dialog onCreateDialog(int id, Bundle bundle) {
         switch (id) {
             case DIALOG_CONFIRM_DELETE:
@@ -341,25 +342,15 @@
                         .setPositiveButton(android.R.string.ok, new DeleteClickListener())
                         .setCancelable(false)
                         .create();
+            case DIALOG_PICK_PHOTO:
+                return createPickPhotoDialog();
+            case DIALOG_SPLIT:
+                return createSplitDialog();
+            case DIALOG_SELECT_ACCOUNT:
+                return createSelectAccountDialog(bundle);
+            default:
+                return mDialogManager.onCreateDialog(id, bundle);
         }
-        return null;
-    }
-
-    /**
-     * Start managing this {@link Dialog} along with the {@link Activity}.
-     */
-    private void startManagingDialog(Dialog dialog) {
-        synchronized (mManagedDialogs) {
-            mManagedDialogs.add(dialog);
-        }
-    }
-
-    /**
-     * Show this {@link Dialog} and manage with the {@link Activity}.
-     */
-    void showAndManageDialog(Dialog dialog) {
-        startManagingDialog(dialog);
-        dialog.show();
     }
 
     /**
@@ -856,8 +847,8 @@
         }
 
         mContactIdForJoin = ContentUris.parseId(contactLookupUri);
-        Intent intent = new Intent(ContactsListActivity.JOIN_AGGREGATE);
-        intent.putExtra(ContactsListActivity.EXTRA_AGGREGATE_ID, mContactIdForJoin);
+        Intent intent = new Intent(JoinContactActivity.JOIN_CONTACT);
+        intent.putExtra(JoinContactActivity.EXTRA_TARGET_CONTACT_ID, mContactIdForJoin);
         startActivityForResult(intent, REQUEST_JOIN_CONTACT);
     }
 
@@ -1021,7 +1012,7 @@
 
         mRawContactIdRequestingPhoto = rawContactId;
 
-        showAndManageDialog(createPickPhotoDialog());
+        showDialog(DIALOG_PICK_PHOTO);
 
         return true;
     }
@@ -1036,8 +1027,7 @@
         final Context dialogContext = new ContextThemeWrapper(context,
                 android.R.style.Theme_Light);
 
-        String[] choices;
-        choices = new String[2];
+        String[] choices = new String[2];
         choices[0] = getString(R.string.take_photo);
         choices[1] = getString(R.string.pick_photo);
         final ListAdapter adapter = new ArrayAdapter<String>(dialogContext,
@@ -1167,7 +1157,7 @@
     private boolean doSplitContactAction() {
         if (!hasValidState()) return false;
 
-        showAndManageDialog(createSplitDialog());
+        showDialog(DIALOG_SPLIT);
         return true;
     }
 
@@ -1229,6 +1219,14 @@
             return;  // Don't show a dialog.
         }
 
+        Bundle bundle = new Bundle();
+        bundle.putParcelableArrayList(BUNDLE_SELECT_ACCOUNT_LIST, accounts);
+        showDialog(DIALOG_SELECT_ACCOUNT, bundle);
+    }
+
+    private Dialog createSelectAccountDialog(Bundle bundle) {
+        final ArrayList<Account> accounts = bundle.getParcelableArrayList(
+                BUNDLE_SELECT_ACCOUNT_LIST);
         // Wrap our context to inflate list items using correct theme
         final Context dialogContext = new ContextThemeWrapper(this, android.R.style.Theme_Light);
         final LayoutInflater dialogInflater =
@@ -1285,7 +1283,13 @@
         builder.setTitle(R.string.dialog_new_contact_account);
         builder.setSingleChoiceItems(accountAdapter, 0, clickListener);
         builder.setOnCancelListener(cancelListener);
-        showAndManageDialog(builder.create());
+        final Dialog result = builder.create();
+        result.setOnDismissListener(new OnDismissListener() {
+            public void onDismiss(DialogInterface dialog) {
+                removeDialog(DIALOG_SELECT_ACCOUNT);
+            }
+        });
+        return result;
     }
 
     /**
@@ -1315,12 +1319,6 @@
         EntityModifier.ensureKindExists(insert, source, Phone.CONTENT_ITEM_TYPE);
         EntityModifier.ensureKindExists(insert, source, Email.CONTENT_ITEM_TYPE);
 
-        // Create "My Contacts" membership for Google contacts
-        // TODO: move this off into "templates" for each given source
-        if (GoogleSource.ACCOUNT_TYPE.equals(source.accountType)) {
-            GoogleSource.attemptMyContactsMembership(insert, this);
-        }
-
         if (mState == null) {
             // Create state if none exists yet
             mState = EntitySet.fromSingle(insert);
@@ -1409,4 +1407,8 @@
             ContactsSearchManager.startSearch(this, initialQuery);
         }
     }
+
+    public DialogManager getDialogManager() {
+        return mDialogManager;
+    }
 }
diff --git a/src/com/android/contacts/ui/widget/ContactEditorView.java b/src/com/android/contacts/ui/widget/ContactEditorView.java
index 83bf2fb..0916bc0 100644
--- a/src/com/android/contacts/ui/widget/ContactEditorView.java
+++ b/src/com/android/contacts/ui/widget/ContactEditorView.java
@@ -25,13 +25,15 @@
 import com.android.contacts.model.Editor.EditorListener;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
 import com.android.contacts.ui.ViewIdGenerator;
+import com.android.contacts.util.DialogManager;
+import com.android.contacts.util.DialogManager.DialogShowingView;
 
+import android.app.AlertDialog;
+import android.app.Dialog;
 import android.content.Context;
+import android.content.DialogInterface;
 import android.content.Entity;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.os.Parcel;
-import android.os.Parcelable;
+import android.os.Bundle;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.RawContacts;
@@ -39,49 +41,46 @@
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.text.TextUtils;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.View.OnClickListener;
+import android.widget.Button;
 import android.widget.ImageView;
 import android.widget.TextView;
 
+import java.util.ArrayList;
+
 /**
  * Custom view that provides all the editor interaction for a specific
  * {@link Contacts} represented through an {@link EntityDelta}. Callers can
  * reuse this view and quickly rebuild its contents through
- * {@link #setState(EntityDelta, ContactsSource)}.
+ * {@link #setState(EntityDelta, ContactsSource, ViewIdGenerator)}.
  * <p>
  * Internal updates are performed against {@link ValuesDelta} so that the
  * source {@link Entity} can be swapped out. Any state-based changes, such as
  * adding {@link Data} rows or changing {@link EditType}, are performed through
  * {@link EntityModifier} to ensure that {@link ContactsSource} are enforced.
  */
-public class ContactEditorView extends BaseContactEditorView implements OnClickListener {
-    private TextView mReadOnly;
-    private TextView mReadOnlyName;
-
+public class ContactEditorView extends BaseContactEditorView implements DialogShowingView {
     private View mPhotoStub;
     private GenericEditorView mName;
 
-    private boolean mIsSourceReadOnly;
-    private ViewGroup mGeneral;
-    private ViewGroup mSecondary;
-    private boolean mSecondaryVisible;
+    private ViewGroup mFields;
 
-    private TextView mSecondaryHeader;
-
-    private Drawable mSecondaryOpen;
-    private Drawable mSecondaryClosed;
-
-    private View mHeaderColorBar;
-    private View mSideBar;
     private ImageView mHeaderIcon;
     private TextView mHeaderAccountType;
     private TextView mHeaderAccountName;
 
+    private Button mAddFieldButton;
+
     private long mRawContactId = -1;
 
+    private DialogManager mDialogManager = null;
+
+    private static final String DIALOG_ID_KEY = "dialog_id";
+    private static final int DIALOG_ID_FIELD_SELECTOR = 1;
+
     public ContactEditorView(Context context) {
         super(context);
     }
@@ -95,66 +94,29 @@
     protected void onFinishInflate() {
         super.onFinishInflate();
 
-        mInflater = (LayoutInflater)getContext().getSystemService(
-                Context.LAYOUT_INFLATER_SERVICE);
+        mInflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
 
         mPhoto = (PhotoEditorView)findViewById(R.id.edit_photo);
         mPhotoStub = findViewById(R.id.stub_photo);
 
         final int photoSize = getResources().getDimensionPixelSize(R.dimen.edit_photo_size);
 
-        mReadOnly = (TextView)findViewById(R.id.edit_read_only);
-
         mName = (GenericEditorView)findViewById(R.id.edit_name);
         mName.setMinimumHeight(photoSize);
         mName.setDeletable(false);
 
-        mReadOnlyName = (TextView) findViewById(R.id.read_only_name);
+        mFields = (ViewGroup)findViewById(R.id.sect_fields);
 
-        mGeneral = (ViewGroup)findViewById(R.id.sect_general);
-        mSecondary = (ViewGroup)findViewById(R.id.sect_secondary);
-
-        mHeaderColorBar = findViewById(R.id.header_color_bar);
-        mSideBar = findViewById(R.id.color_bar);
         mHeaderIcon = (ImageView) findViewById(R.id.header_icon);
         mHeaderAccountType = (TextView) findViewById(R.id.header_account_type);
         mHeaderAccountName = (TextView) findViewById(R.id.header_account_name);
 
-        mSecondaryHeader = (TextView)findViewById(R.id.head_secondary);
-        mSecondaryHeader.setOnClickListener(this);
-
-        final Resources res = getResources();
-        mSecondaryOpen = res.getDrawable(com.android.internal.R.drawable.expander_ic_maximized);
-        mSecondaryClosed = res.getDrawable(com.android.internal.R.drawable.expander_ic_minimized);
-
-        this.setSecondaryVisible(false);
-    }
-
-    /** {@inheritDoc} */
-    public void onClick(View v) {
-        // Toggle visibility of secondary kinds
-        final boolean makeVisible = mSecondary.getVisibility() != View.VISIBLE;
-        this.setSecondaryVisible(makeVisible);
-    }
-
-    /**
-     * Set the visibility of secondary sections, along with header icon.
-     *
-     * <p>If the source is read-only and there's no secondary fields, the entire secondary section
-     * will be hidden.
-     */
-    private void setSecondaryVisible(boolean makeVisible) {
-        mSecondaryVisible = makeVisible;
-
-        if (!mIsSourceReadOnly && mSecondary.getChildCount() > 0) {
-            mSecondaryHeader.setVisibility(View.VISIBLE);
-            mSecondaryHeader.setCompoundDrawablesWithIntrinsicBounds(
-                    makeVisible ? mSecondaryOpen : mSecondaryClosed, null, null, null);
-            mSecondary.setVisibility(makeVisible ? View.VISIBLE : View.GONE);
-        } else {
-            mSecondaryHeader.setVisibility(View.GONE);
-            mSecondary.setVisibility(View.GONE);
-        }
+        mAddFieldButton = (Button) findViewById(R.id.button_add_field);
+        mAddFieldButton.setOnClickListener(new OnClickListener() {
+            public void onClick(View v) {
+                showDialog(DIALOG_ID_FIELD_SELECTOR);
+            }
+        });
     }
 
     /**
@@ -165,16 +127,13 @@
     @Override
     public void setState(EntityDelta state, ContactsSource source, ViewIdGenerator vig) {
         // Remove any existing sections
-        mGeneral.removeAllViews();
-        mSecondary.removeAllViews();
+        mFields.removeAllViews();
 
         // Bail if invalid state or source
         if (state == null || source == null) return;
 
         setId(vig.getId(state, null, null, ViewIdGenerator.NO_VIEW_INDEX));
 
-        mIsSourceReadOnly = source.readOnly;
-
         // Make sure we have StructuredName
         EntityModifier.ensureKindExists(state, source, StructuredName.CONTENT_ITEM_TYPE);
 
@@ -198,24 +157,13 @@
         EntityModifier.ensureKindExists(state, source, Photo.CONTENT_ITEM_TYPE);
         mHasPhotoEditor = (source.getKindForMimetype(Photo.CONTENT_ITEM_TYPE) != null);
         mPhoto.setVisibility(mHasPhotoEditor ? View.VISIBLE : View.GONE);
-        mPhoto.setEnabled(!mIsSourceReadOnly);
-        mName.setEnabled(!mIsSourceReadOnly);
+        mPhoto.setEnabled(true);
+        mName.setEnabled(true);
 
         // Show and hide the appropriate views
-        if (mIsSourceReadOnly) {
-            mGeneral.setVisibility(View.GONE);
-            mName.setVisibility(View.GONE);
-            mReadOnly.setVisibility(View.VISIBLE);
-            mReadOnly.setText(mContext.getString(R.string.contact_read_only, accountType));
-            mReadOnlyName.setVisibility(View.VISIBLE);
-        } else {
-            mGeneral.setVisibility(View.VISIBLE);
-            mName.setVisibility(View.VISIBLE);
-            mReadOnly.setVisibility(View.GONE);
-            mReadOnlyName.setVisibility(View.GONE);
-        }
+        mFields.setVisibility(View.VISIBLE);
+        mName.setVisibility(View.VISIBLE);
 
-        boolean anySecondaryFieldFilled = false;
         // Create editor sections for each possible data kind
         for (DataKind kind : source.getSortedDataKinds()) {
             // Skip kind of not editable
@@ -225,36 +173,21 @@
             if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
                 // Handle special case editor for structured name
                 final ValuesDelta primary = state.getPrimaryEntry(mimeType);
-                if (!mIsSourceReadOnly) {
-                    mName.setValues(kind, primary, state, mIsSourceReadOnly, vig);
-                } else {
-                    String displayName = primary.getAsString(StructuredName.DISPLAY_NAME);
-                    mReadOnlyName.setText(displayName);
-                }
+                mName.setValues(kind, primary, state, false, vig);
             } else if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
                 // Handle special case editor for photos
                 final ValuesDelta primary = state.getPrimaryEntry(mimeType);
-                mPhoto.setValues(kind, primary, state, mIsSourceReadOnly, vig);
-                if (mIsSourceReadOnly && !mPhoto.hasSetPhoto()) {
-                    mPhotoStub.setVisibility(View.GONE);
-                } else {
-                    mPhotoStub.setVisibility(View.VISIBLE);
-                }
-            } else if (!mIsSourceReadOnly) {
+                mPhoto.setValues(kind, primary, state, false, vig);
+                mPhotoStub.setVisibility(View.VISIBLE);
+            } else {
                 // Otherwise use generic section-based editors
                 if (kind.fieldList == null) continue;
-                final ViewGroup parent = kind.secondary ? mSecondary : mGeneral;
                 final KindSectionView section = (KindSectionView)mInflater.inflate(
-                        R.layout.item_kind_section, parent, false);
-                section.setState(kind, state, mIsSourceReadOnly, vig);
-                if (kind.secondary && section.isAnyEditorFilledOut()) {
-                    anySecondaryFieldFilled = true;
-                }
-                parent.addView(section);
+                        R.layout.item_kind_section, mFields, false);
+                section.setState(kind, state, false, vig);
+                mFields.addView(section);
             }
         }
-
-        setSecondaryVisible(anySecondaryFieldFilled);
     }
 
     /**
@@ -270,56 +203,53 @@
         return mRawContactId;
     }
 
-    private static class SavedState extends BaseSavedState {
-        public boolean mSecondaryVisible;
-
-        SavedState(Parcelable superState) {
-            super(superState);
-        }
-
-        private SavedState(Parcel in) {
-            super(in);
-            mSecondaryVisible = (in.readInt() == 0 ? false : true);
-        }
-
-        @Override
-        public void writeToParcel(Parcel out, int flags) {
-            super.writeToParcel(out, flags);
-            out.writeInt(mSecondaryVisible ? 1 : 0);
-        }
-
-        public static final Parcelable.Creator<SavedState> CREATOR
-                = new Parcelable.Creator<SavedState>() {
-            public SavedState createFromParcel(Parcel in) {
-                return new SavedState(in);
-            }
-
-            public SavedState[] newArray(int size) {
-                return new SavedState[size];
-            }
-        };
+    /* package */
+    void showDialog(int bundleDialogId) {
+        final Bundle bundle = new Bundle();
+        bundle.putInt(DIALOG_ID_KEY, bundleDialogId);
+        getDialogManager().showDialogInView(this, bundle);
     }
 
-    /**
-     * Saves the visibility of the secondary field.
-     */
-    @Override
-    protected Parcelable onSaveInstanceState() {
-        Parcelable superState = super.onSaveInstanceState();
-        SavedState ss = new SavedState(superState);
-
-        ss.mSecondaryVisible = mSecondaryVisible;
-        return ss;
+    private DialogManager getDialogManager() {
+        if (mDialogManager == null) {
+            Context context = getContext();
+            if (!(context instanceof DialogManager.DialogShowingViewActivity)) {
+                throw new IllegalStateException(
+                        "View must be hosted in an Activity that implements " +
+                        "DialogManager.DialogShowingViewActivity");
+            }
+            mDialogManager = ((DialogManager.DialogShowingViewActivity)context).getDialogManager();
+        }
+        return mDialogManager;
     }
 
-    /**
-     * Restores the visibility of the secondary field.
-     */
-    @Override
-    protected void onRestoreInstanceState(Parcelable state) {
-        SavedState ss = (SavedState) state;
-        super.onRestoreInstanceState(ss.getSuperState());
-
-        setSecondaryVisible(ss.mSecondaryVisible);
+    public Dialog createDialog(Bundle bundle) {
+        if (bundle == null) throw new IllegalArgumentException("bundle must not be null");
+        int dialogId = bundle.getInt(DIALOG_ID_KEY);
+        switch (dialogId) {
+            case DIALOG_ID_FIELD_SELECTOR:
+                final ArrayList<CharSequence> items =
+                        new ArrayList<CharSequence>(mFields.getChildCount());
+                for (int i = 0; i < mFields.getChildCount(); i++) {
+                    final KindSectionView sectionView = (KindSectionView) mFields.getChildAt(i);
+                    // not a list and already exists? ignore
+                    if (!sectionView.getKind().isList && sectionView.getEditorCount() != 0) {
+                        continue;
+                    }
+                    items.add(sectionView.getTitle());
+                }
+                final DialogInterface.OnClickListener itemClickListener =
+                        new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int which) {
+                        final KindSectionView view = (KindSectionView) mFields.getChildAt(which);
+                        view.addItem();
+                    }
+                };
+                return new AlertDialog.Builder(getContext())
+                        .setItems(items.toArray(new CharSequence[0]), itemClickListener)
+                        .create();
+            default:
+                throw new IllegalArgumentException("Invalid dialogId: " + dialogId);
+        }
     }
 }
diff --git a/src/com/android/contacts/ui/widget/DontPressWithParentImageView.java b/src/com/android/contacts/ui/widget/DontPressWithParentImageView.java
deleted file mode 100644
index bdb0e0a..0000000
--- a/src/com/android/contacts/ui/widget/DontPressWithParentImageView.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.contacts.ui.widget;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.ImageView;
-
-/**
- * Special class to to allow the parent to be pressed without being pressed itself.
- * This way the line of a tab can be pressed, but the image itself is not.
- */
-public class DontPressWithParentImageView extends ImageView {
-
-    public DontPressWithParentImageView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    @Override
-    public void setPressed(boolean pressed) {
-        // If the parent is pressed, do not set to pressed.
-        if (pressed && ((View) getParent()).isPressed()) {
-            return;
-        }
-        super.setPressed(pressed);
-    }
-}
diff --git a/src/com/android/contacts/ui/widget/GenericEditorView.java b/src/com/android/contacts/ui/widget/GenericEditorView.java
index 24262bb..5c2f9b7 100644
--- a/src/com/android/contacts/ui/widget/GenericEditorView.java
+++ b/src/com/android/contacts/ui/widget/GenericEditorView.java
@@ -26,12 +26,16 @@
 import com.android.contacts.model.ContactsSource.EditType;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
 import com.android.contacts.ui.ViewIdGenerator;
+import com.android.contacts.util.ViewGroupAnimator;
+import com.android.contacts.util.DialogManager;
+import com.android.contacts.util.DialogManager.DialogShowingView;
 
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Entity;
+import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.telephony.PhoneNumberFormattingTextWatcher;
@@ -47,6 +51,7 @@
 import android.view.inputmethod.EditorInfo;
 import android.widget.ArrayAdapter;
 import android.widget.EditText;
+import android.widget.ImageButton;
 import android.widget.ListAdapter;
 import android.widget.RelativeLayout;
 import android.widget.TextView;
@@ -58,10 +63,15 @@
  * the entry. Uses {@link ValuesDelta} to read any existing
  * {@link Entity} values, and to correctly write any changes values.
  */
-public class GenericEditorView extends RelativeLayout implements Editor, View.OnClickListener {
+public class GenericEditorView extends RelativeLayout implements Editor, View.OnClickListener,
+        DialogShowingView {
     protected static final int RES_FIELD = R.layout.item_editor_field;
     protected static final int RES_LABEL_ITEM = android.R.layout.simple_list_item_1;
 
+    private static final String DIALOG_ID_KEY = "dialog_id";
+    private static final int DIALOG_ID_LABEL = 1;
+    private static final int DIALOG_ID_CUSTOM = 2;
+
     protected LayoutInflater mInflater;
 
     protected static final int INPUT_TYPE_CUSTOM = EditorInfo.TYPE_CLASS_TEXT
@@ -70,8 +80,7 @@
     protected TextView mLabel;
     protected ViewGroup mFields;
     protected View mDelete;
-    protected View mMore;
-    protected View mLess;
+    protected ImageButton mMoreOrLess;
 
     protected DataKind mKind;
     protected ValuesDelta mEntry;
@@ -85,6 +94,7 @@
     private EditType mPendingType;
 
     private ViewIdGenerator mViewIdGenerator;
+    private DialogManager mDialogManager = null;
 
     public GenericEditorView(Context context) {
         super(context);
@@ -108,11 +118,8 @@
         mDelete = findViewById(R.id.edit_delete);
         mDelete.setOnClickListener(this);
 
-        mMore = findViewById(R.id.edit_more);
-        mMore.setOnClickListener(this);
-
-        mLess = findViewById(R.id.edit_less);
-        mLess.setOnClickListener(this);
+        mMoreOrLess = (ImageButton) findViewById(R.id.edit_more_or_less);
+        mMoreOrLess.setOnClickListener(this);
     }
 
     protected EditorListener mListener;
@@ -133,8 +140,7 @@
             final View v = mFields.getChildAt(pos);
             v.setEnabled(enabled);
         }
-        mMore.setEnabled(enabled);
-        mLess.setEnabled(enabled);
+        mMoreOrLess.setEnabled(enabled);
     }
 
     /**
@@ -266,14 +272,13 @@
 
         // When hiding fields, place expandable
         if (hidePossible) {
-            mMore.setVisibility(mHideOptional ? View.VISIBLE : View.GONE);
-            mLess.setVisibility(mHideOptional ? View.GONE : View.VISIBLE);
+            mMoreOrLess.setVisibility(View.VISIBLE);
+            mMoreOrLess.setImageResource(
+                    mHideOptional ? R.drawable.ic_btn_round_more : R.drawable.ic_btn_round_less);
         } else {
-            mMore.setVisibility(View.GONE);
-            mLess.setVisibility(View.GONE);
+            mMoreOrLess.setVisibility(View.GONE);
         }
-        mMore.setEnabled(enabled);
-        mLess.setEnabled(enabled);
+        mMoreOrLess.setEnabled(enabled);
     }
 
     /**
@@ -354,7 +359,7 @@
                     // Only when the custum value input in the next step is correct one.
                     // this method also set the type value to what the user requested here.
                     mPendingType = selected;
-                    createCustomDialog().show();
+                    showDialog(DIALOG_ID_CUSTOM);
                 } else {
                     // User picked type, and we're sure it's ok to actually write the entry.
                     mType = selected;
@@ -376,32 +381,75 @@
     public void onClick(View v) {
         switch (v.getId()) {
             case R.id.edit_label: {
-                createLabelDialog().show();
+                showDialog(DIALOG_ID_LABEL);
                 break;
             }
             case R.id.edit_delete: {
                 // Keep around in model, but mark as deleted
                 mEntry.markDeleted();
 
-                // Remove editor from parent view
-                final ViewGroup parent = (ViewGroup)getParent();
-                parent.removeView(this);
+//                final ViewGroupAnimator animator = ViewGroupAnimator.captureView(getRootView());
+
+//                animator.removeView(this);
+                ((ViewGroup) getParent()).removeView(this);
 
                 if (mListener != null) {
                     // Notify listener when present
                     mListener.onDeleted(this);
                 }
+
+//                animator.animate();
                 break;
             }
-            case R.id.edit_more:
-            case R.id.edit_less: {
+            case R.id.edit_more_or_less: {
+                // Save focus
+                final View focusedChild = mFields.getFocusedChild();
+                final int focusedViewId = focusedChild == null ? -1 : focusedChild.getId();
+
+                // Snapshot for animation
+//                final ViewGroupAnimator animator = ViewGroupAnimator.captureView(getRootView());
+
+                // Reconfigure GUI
                 mHideOptional = !mHideOptional;
                 rebuildValues();
+
+                // Restore focus
+                View newFocusView = mFields.findViewById(focusedViewId);
+                if (newFocusView == null || newFocusView.getVisibility() == GONE) {
+                    // find first visible child
+                    newFocusView = this;
+                }
+                if (newFocusView != null) {
+                    newFocusView.requestFocus();
+                }
+
+                // Animate
+//                animator.animate();
                 break;
             }
         }
     }
 
+    /* package */
+    void showDialog(int bundleDialogId) {
+        Bundle bundle = new Bundle();
+        bundle.putInt(DIALOG_ID_KEY, bundleDialogId);
+        getDialogManager().showDialogInView(this, bundle);
+    }
+
+    private DialogManager getDialogManager() {
+        if (mDialogManager == null) {
+            Context context = getContext();
+            if (!(context instanceof DialogManager.DialogShowingViewActivity)) {
+                throw new IllegalStateException(
+                        "View must be hosted in an Activity that implements " +
+                        "DialogManager.DialogShowingViewActivity");
+            }
+            mDialogManager = ((DialogManager.DialogShowingViewActivity)context).getDialogManager();
+        }
+        return mDialogManager;
+    }
+
     private static class SavedState extends BaseSavedState {
         public boolean mHideOptional;
         public int[] mVisibilities;
@@ -469,4 +517,17 @@
             mFields.getChildAt(i).setVisibility(ss.mVisibilities[i]);
         }
     }
+
+    public Dialog createDialog(Bundle bundle) {
+        if (bundle == null) throw new IllegalArgumentException("bundle must not be null");
+        int dialogId = bundle.getInt(DIALOG_ID_KEY);
+        switch (dialogId) {
+            case DIALOG_ID_CUSTOM:
+                return createCustomDialog();
+            case DIALOG_ID_LABEL:
+                return createLabelDialog();
+            default:
+                throw new IllegalArgumentException("Invalid dialogId: " + dialogId);
+        }
+    }
 }
diff --git a/src/com/android/contacts/ui/widget/KindSectionView.java b/src/com/android/contacts/ui/widget/KindSectionView.java
index 221bc16..88f88c7 100644
--- a/src/com/android/contacts/ui/widget/KindSectionView.java
+++ b/src/com/android/contacts/ui/widget/KindSectionView.java
@@ -31,7 +31,6 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.View.OnClickListener;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
@@ -41,7 +40,7 @@
  * {@link DataKind} around a {@link Data#MIMETYPE}. This view shows a
  * section header and a trigger for adding new {@link Data} rows.
  */
-public class KindSectionView extends LinearLayout implements OnClickListener, EditorListener {
+public class KindSectionView extends LinearLayout implements EditorListener {
     private static final String TAG = "KindSectionView";
 
     private LayoutInflater mInflater;
@@ -77,7 +76,11 @@
         mEditors = (ViewGroup)findViewById(R.id.kind_editors);
 
         mAdd = findViewById(R.id.kind_header);
-        mAdd.setOnClickListener(this);
+        mAdd.setOnClickListener(new OnClickListener() {
+            public void onClick(View v) {
+                addItem();
+            }
+        });
 
         mAddPlusButton = (ImageView) findViewById(R.id.kind_plus);
 
@@ -86,8 +89,9 @@
 
     /** {@inheritDoc} */
     public void onDeleted(Editor editor) {
-        this.updateAddEnabled();
-        this.updateEditorsVisible();
+        updateAddEnabled();
+        updateEditorsVisible();
+        updateVisible();
     }
 
     /** {@inheritDoc} */
@@ -109,12 +113,17 @@
         // Only show the add button if this is a list
         mAddPlusButton.setVisibility(mKind.isList ? View.VISIBLE : View.GONE);
 
-        this.rebuildFromState();
-        this.updateAddEnabled();
-        this.updateEditorsVisible();
+        rebuildFromState();
+        updateAddEnabled();
+        updateEditorsVisible();
+        updateVisible();
     }
 
-    public boolean isAnyEditorFilledOut() {
+    public CharSequence getTitle() {
+        return mTitle.getText();
+    }
+
+    public boolean getFieldCount() {
         if (mState == null) {
             return false;
         }
@@ -123,7 +132,7 @@
             return false;
         }
 
-        int editorCount = mEditors.getChildCount();
+        int editorCount = getEditorCount();
         for (int i = 0; i < editorCount; i++) {
             GenericEditorView editorView = (GenericEditorView) mEditors.getChildAt(i);
             if (editorView.isAnyFieldFilledOut()) {
@@ -144,23 +153,6 @@
         // Check if we are displaying anything here
         boolean hasEntries = mState.hasMimeEntries(mKind.mimeType);
 
-        if (!mKind.isList) {
-            if (hasEntries) {
-                // we might have no visible entries. check that, too
-                for (ValuesDelta entry : mState.getMimeEntries(mKind.mimeType)) {
-                    if (!entry.isVisible()) {
-                        hasEntries = false;
-                        break;
-                    }
-                }
-            }
-
-            if (!hasEntries) {
-                EntityModifier.insertChild(mState, mKind);
-                hasEntries = true;
-            }
-        }
-
         if (hasEntries) {
             int entryIndex = 0;
             for (ValuesDelta entry : mState.getMimeEntries(mKind.mimeType)) {
@@ -170,11 +162,6 @@
                 final GenericEditorView editor = (GenericEditorView)mInflater.inflate(
                         R.layout.item_generic_editor, mEditors, false);
                 editor.setValues(mKind, entry, mState, mReadOnly, mViewIdGenerator);
-                // older versions of android had lists where we now have a single value
-                // in these cases we should show the remove button for all but the first value
-                // to ensure that nothing is removed
-                editor.mDelete.setVisibility((mKind.isList || (entryIndex != 0))
-                        ? View.VISIBLE : View.GONE);
                 editor.setEditorListener(this);
                 mEditors.addView(editor);
                 entryIndex++;
@@ -183,10 +170,15 @@
     }
 
     protected void updateEditorsVisible() {
-        final boolean hasChildren = mEditors.getChildCount() > 0;
+        final boolean hasChildren = getEditorCount() > 0;
         mEditors.setVisibility(hasChildren ? View.VISIBLE : View.GONE);
     }
 
+    private void updateVisible() {
+        setVisibility(getEditorCount() != 0 ? VISIBLE : GONE);
+    }
+
+
     protected void updateAddEnabled() {
         // Set enabled state on the "add" view
         final boolean canInsert = EntityModifier.canInsert(mState, mKind);
@@ -194,18 +186,18 @@
         mAdd.setEnabled(isEnabled);
     }
 
-    /** {@inheritDoc} */
-    public void onClick(View v) {
-        // if this is not a list the plus button is not visible but the user might have clicked
-        // the text.
-        if (!mKind.isList)
+    public void addItem() {
+        // if this is a list, we can freely add. if not, only allow adding the first
+        if (!mKind.isList && getEditorCount() == 1)
             return;
 
+//        final ViewGroupAnimator animator = ViewGroupAnimator.captureView(getRootView());
+
         // Insert a new child and rebuild
         final ValuesDelta newValues = EntityModifier.insertChild(mState, mKind);
-        this.rebuildFromState();
-        this.updateAddEnabled();
-        this.updateEditorsVisible();
+        rebuildFromState();
+        updateAddEnabled();
+        updateEditorsVisible();
 
         // Find the newly added EditView and set focus.
         final int newFieldId = mViewIdGenerator.getId(mState, mKind, newValues, 0);
@@ -213,5 +205,17 @@
         if (newField != null) {
             newField.requestFocus();
         }
+
+        updateVisible();
+
+//        animator.animate();
+    }
+
+    public int getEditorCount() {
+        return mEditors.getChildCount();
+    }
+
+    public DataKind getKind() {
+        return mKind;
     }
 }
diff --git a/src/com/android/contacts/util/AccountSelectionUtil.java b/src/com/android/contacts/util/AccountSelectionUtil.java
index cc46d2b..6f1d851 100644
--- a/src/com/android/contacts/util/AccountSelectionUtil.java
+++ b/src/com/android/contacts/util/AccountSelectionUtil.java
@@ -163,10 +163,6 @@
     }
 
     public static void doImportFromSim(Context context, Account account) {
-        if (account != null) {
-            GoogleSource.createMyContactsIfNotExist(account, context);
-        }
-
         Intent importIntent = new Intent(Intent.ACTION_VIEW);
         importIntent.setType("vnd.android.cursor.item/sim-contact");
         if (account != null) {
@@ -178,10 +174,6 @@
     }
 
     public static void doImportFromSdCard(Context context, Account account) {
-        if (account != null) {
-            GoogleSource.createMyContactsIfNotExist(account, context);
-        }
-
         Intent importIntent = new Intent(context, ImportVCardActivity.class);
         if (account != null) {
             importIntent.putExtra("account_name", account.name);
diff --git a/src/com/android/contacts/util/Constants.java b/src/com/android/contacts/util/Constants.java
index e0178ad..433d54d 100644
--- a/src/com/android/contacts/util/Constants.java
+++ b/src/com/android/contacts/util/Constants.java
@@ -19,11 +19,6 @@
 import android.app.Service;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 
-/**
- * Background {@link Service} that is used to keep our process alive long enough
- * for background threads to finish. Started and stopped directly by specific
- * background tasks when needed.
- */
 public class Constants {
     /**
      * Specific MIME-type for {@link Phone#CONTENT_ITEM_TYPE} entries that
diff --git a/src/com/android/contacts/util/DialogManager.java b/src/com/android/contacts/util/DialogManager.java
new file mode 100644
index 0000000..4c6baf3
--- /dev/null
+++ b/src/com/android/contacts/util/DialogManager.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.util;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnDismissListener;
+import android.os.Bundle;
+import android.view.View;
+
+/**
+ * Manages creation and destruction of Dialogs that are to be shown by Views. Unlike how Dialogs
+ * are regularly used, the Dialogs are not recycled but immediately destroyed after dismissal.
+ * To be able to do that, two IDs are required which are used consecutively.
+ * How to use:<ul>
+ * <li>The owning Activity creates on instance of this class, passing itself and two Ids that are
+ *    not used by other Dialogs of the Activity.</li>
+ * <li>Views owning Dialogs must implement {@link DialogManager.DialogShowingView}</li>
+ * <li>After creating the Views, configureManagingViews must be called to configure all views
+ *    that implement {@link DialogManager.DialogShowingView}</li>
+ * <li>In the implementation of {@link Activity#onCreateDialog}, calls for the
+ *    ViewId are forwarded to {@link DialogManager#onCreateDialog(int, Bundle)}</li>
+ * </ul>
+ * To actually show a Dialog, the View uses {@link DialogManager#showDialogInView(View, Bundle)},
+ * passing itself as a first parameter
+ */
+public class DialogManager {
+    private final Activity mActivity;
+    private final int mDialogId1;
+    private final int mDialogId2;
+    private boolean mUseDialogId2 = false;
+    public final static String VIEW_ID_KEY = "view_id";
+
+    /**
+     * Creates a new instance of this class for the given Activity.
+     * @param activity The activity this object is used for
+     * @param dialogId1 The first Id that is reserved for use by child-views
+     * @param dialogId2 The second Id that is reserved for use by child-views
+     */
+    public DialogManager(final Activity activity, final int dialogId1, final int dialogId2) {
+        if (activity == null) throw new IllegalArgumentException("activity must not be null");
+        if (dialogId1 == dialogId2) throw new IllegalArgumentException("Ids must be different");
+        mActivity = activity;
+        mDialogId1 = dialogId1;
+        mDialogId2 = dialogId2;
+    }
+
+    /**
+     * Called by a View to show a dialog. It has to pass itself and a Bundle with extra information.
+     * If the view can show several dialogs, it should distinguish them using an item in the Bundle.
+     * The View needs to have a valid and unique Id. This function modifies the bundle by adding a
+     * new item named {@link DialogManager#VIEW_ID_KEY}
+     */
+    public void showDialogInView(final View view, final Bundle bundle) {
+        final int viewId = view.getId();
+        if (bundle.containsKey(VIEW_ID_KEY)) {
+            throw new IllegalArgumentException("Bundle already contains a " + VIEW_ID_KEY);
+        }
+        if (viewId == View.NO_ID) {
+            throw new IllegalArgumentException("View does not have a proper ViewId");
+        }
+        bundle.putInt(VIEW_ID_KEY, viewId);
+        int dialogId = mUseDialogId2 ? mDialogId2 : mDialogId1;
+        mActivity.showDialog(dialogId, bundle);
+    }
+
+    /**
+     * Callback function called by the Activity to handle View-managed Dialogs.
+     * This function returns null if the id is not one of the two reserved Ids.
+     */
+    public Dialog onCreateDialog(final int id, final Bundle bundle) {
+        if (id == mDialogId1) {
+            mUseDialogId2 = true;
+        } else if (id == mDialogId2) {
+            mUseDialogId2 = false;
+        } else {
+            return null;
+        }
+        if (!bundle.containsKey(VIEW_ID_KEY)) {
+            throw new IllegalArgumentException("Bundle does not contain a ViewId");
+        }
+        final int viewId = bundle.getInt(VIEW_ID_KEY);
+        final View view = mActivity.findViewById(viewId);
+        if (view == null || !(view instanceof DialogShowingView)) {
+            return null;
+        }
+        final Dialog dialog = ((DialogShowingView)view).createDialog(bundle);
+
+        // As we will never re-use this dialog, we can completely kill it here
+        dialog.setOnDismissListener(new OnDismissListener() {
+            public void onDismiss(DialogInterface dialogInterface) {
+                mActivity.removeDialog(id);
+            }
+        });
+        return dialog;
+    }
+
+    /**
+     * Interface to implemented by Views that show Dialogs
+     */
+    public interface DialogShowingView {
+        /**
+         * Callback function to create a Dialog. Notice that the DialogManager overwrites the
+         * OnDismissListener on the returned Dialog, so the View should not use this Listener itself
+         */
+        Dialog createDialog(Bundle bundle);
+    }
+
+    /**
+     * Interface to implemented by Activities that host View-showing dialogs
+     */
+    public interface DialogShowingViewActivity {
+        DialogManager getDialogManager();
+    }
+}
diff --git a/src/com/android/contacts/util/LocalizedNameResolver.java b/src/com/android/contacts/util/LocalizedNameResolver.java
new file mode 100644
index 0000000..c2ee90a
--- /dev/null
+++ b/src/com/android/contacts/util/LocalizedNameResolver.java
@@ -0,0 +1,161 @@
+/*
+ * 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.util;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorDescription;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.content.res.Resources.NotFoundException;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Xml;
+
+import java.io.IOException;
+
+/**
+ * Retrieves localized names per account type. This allows customizing texts like
+ * "All Contacts" for certain account types, but e.g. "All Friends" or "All Connections" for others.
+ */
+public class LocalizedNameResolver  {
+    private static final String TAG = "LocalizedNameResolver";
+
+    /**
+     * Meta-data key for the contacts configuration associated with a sync service.
+     */
+    private static final String METADATA_CONTACTS = "android.provider.CONTACTS_STRUCTURE";
+
+    private static final String CONTACTS_DATA_KIND = "ContactsDataKind";
+
+    /**
+     * Returns the name for All Contacts for the specified account type.
+     */
+    public static String getAllContactsName(Context context, String accountType) {
+        if (context == null) throw new IllegalArgumentException("Context must not be null");
+        if (accountType == null) return null;
+
+        return resolveAllContactsName(context, accountType);
+     }
+
+    /**
+     * Finds "All Contacts"-Name for the specified account type.
+     */
+    private static String resolveAllContactsName(Context context, String accountType) {
+        final AccountManager am = AccountManager.get(context);
+
+        for (AuthenticatorDescription auth : am.getAuthenticatorTypes()) {
+            if (accountType.equals(auth.type)) {
+                return resolveAllContactsNameFromMetaData(context, auth.packageName);
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Finds the meta-data XML containing the contacts configuration and
+     * reads the picture priority from that file.
+     */
+    private static String resolveAllContactsNameFromMetaData(Context context, String packageName) {
+        final PackageManager pm = context.getPackageManager();
+        try {
+            PackageInfo pi = pm.getPackageInfo(packageName, PackageManager.GET_SERVICES
+                    | PackageManager.GET_META_DATA);
+            if (pi != null && pi.services != null) {
+                for (ServiceInfo si : pi.services) {
+                    final XmlResourceParser parser = si.loadXmlMetaData(pm, METADATA_CONTACTS);
+                    if (parser != null) {
+                        return loadAllContactsNameFromXml(context, parser, packageName);
+                    }
+                }
+            }
+        } catch (NameNotFoundException e) {
+            Log.w(TAG, "Problem loading \"All Contacts\"-name: " + e.toString());
+        }
+        return null;
+    }
+
+    private static String loadAllContactsNameFromXml(Context context, XmlPullParser parser,
+            String packageName) {
+        try {
+            final AttributeSet attrs = Xml.asAttributeSet(parser);
+            int type;
+            while ((type = parser.next()) != XmlPullParser.START_TAG
+                    && type != XmlPullParser.END_DOCUMENT) {
+                // Drain comments and whitespace
+            }
+
+            if (type != XmlPullParser.START_TAG) {
+                throw new IllegalStateException("No start tag found");
+            }
+
+            final int depth = parser.getDepth();
+            while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
+                    && type != XmlPullParser.END_DOCUMENT) {
+                String name = parser.getName();
+                if (type == XmlPullParser.START_TAG && CONTACTS_DATA_KIND.equals(name)) {
+                    final TypedArray typedArray = context.obtainStyledAttributes(attrs,
+                            android.R.styleable.ContactsDataKind);
+                    try {
+                        // See if a string has been hardcoded directly into the xml
+                        final String nonResourceString = typedArray.getNonResourceString(
+                                android.R.styleable.ContactsDataKind_allContactsName);
+                        if (nonResourceString != null) {
+                            return nonResourceString;
+                        }
+
+                        // See if a resource is referenced. We can't rely on getString
+                        // to automatically resolve it as the resource lives in a different package
+                        int id = typedArray.getResourceId(
+                                android.R.styleable.ContactsDataKind_allContactsName, 0);
+                        if (id == 0) return null;
+
+                        // Resolve the resource Id
+                        final PackageManager packageManager = context.getPackageManager();
+                        final Resources resources;
+                        try {
+                            resources = packageManager.getResourcesForApplication(packageName);
+                        } catch (NameNotFoundException e) {
+                            return null;
+                        }
+                        try {
+                            return resources.getString(id);
+                        } catch (NotFoundException e) {
+                            return null;
+                        }
+                    } finally {
+                        typedArray.recycle();
+                    }
+                }
+            }
+            return null;
+        } catch (XmlPullParserException e) {
+            throw new IllegalStateException("Problem reading XML", e);
+        } catch (IOException e) {
+            throw new IllegalStateException("Problem reading XML", e);
+        }
+    }
+}
diff --git a/src/com/android/contacts/util/ViewGroupAnimator.java b/src/com/android/contacts/util/ViewGroupAnimator.java
new file mode 100644
index 0000000..83e8a7d
--- /dev/null
+++ b/src/com/android/contacts/util/ViewGroupAnimator.java
@@ -0,0 +1,476 @@
+/*
+ * 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.util;
+
+import android.graphics.Rect;
+import android.os.Message;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewRoot;
+import android.view.ViewTreeObserver.OnPreDrawListener;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.Interpolator;
+import android.view.animation.TranslateAnimation;
+import android.view.animation.Animation.AnimationListener;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+
+/**
+ * Automatically configures natural-feeling animations for views. To use this class,
+ * two calls are required:
+ * <ul>
+ * <li>{@link #captureView(View)} takes a snapshot of all the views and their
+ * positions</li>
+ * <li>{@link #animate()} takes another snapshot, calculates the differences
+ * and creates Translate- and FadeAnimations for the changes</li>
+ * </ul>
+ * To match views, the chain of {@link View#getId()} of each View and all of its parents is
+ * compared. It is therefore not necessary to retain object identity between
+ * {@link #captureView(View)} and {@link #animate()}.
+ * This mechanism works fine for Views that are new or moved. To get a fade out effect for deleted
+ * views, one of two approaches have to be done by the consumer:
+ * <ul>
+ * <li>Instead of actually removing the view, it is only hidden (by setting
+ * its visibility to either {@link View#INVISIBLE} or {@link View#GONE})</li>
+ * <li>{@link #removeView(View)} is used to remove the View. This will hide the view during
+ * the animation and actually remove it from its parent, once the animation is finished.</li>
+ * </ul>
+ * The typical usage pattern looks like this:
+ * <pre>
+ * {@code
+ *   final ViewGroupAnimator a = ViewGroupAnimator.captureView(view);
+ *   // change view here (except for deletions)
+ *   a.removeView(someChildViewThatHasToGo);
+ *   a.animate();}
+ * </pre>
+ */
+// TODO: If we don't have any FadeOuts, we could save 150ms by starting the other animations sooner
+// TODO: Create an interface containing the normal functions so that we can mock this for tests
+public class ViewGroupAnimator {
+    /* package */ static final String TAG = "ViewAnimator";
+
+    private static final OnPreDrawListener CANCEL_DRAW_LISTENER = new OnPreDrawListener() {
+        public boolean onPreDraw() {
+            return false;
+        }
+    };
+
+    private static int MOVE_DURATION_MILLIS_DEFAULT = 250;
+    private static int FADE_DURATION_MILLIS_DEFAULT = 300;
+
+    private static int FADE_OUT_OFFSET_MILLIS_DEFAULT = 0;
+    private static int MOVE_OFFSET_MILLIS_DEFAULT = 150;
+    private static int FADE_IN_OFFSET_MILLIS_DEFAULT = 400;
+
+    private static final Interpolator INTERPOLATOR = new AccelerateDecelerateInterpolator();
+
+    private final View mRootView;
+    private final Snapshot mBeforeSnapshot;
+    private final HashSet<View> mViewsToRemove = new HashSet<View>();
+
+    private Runnable mOnAnimationsFinished;
+
+    /**
+     * Cancels all pending animations by calling {@link Animation#cancel()} for
+     * all animations that are currently attached to the provided view or any of its children.
+     */
+    public static void cancelRunningAnimations(View view) {
+        final Animation animation = view.getAnimation();
+        if (animation != null) {
+            animation.cancel();
+            view.setAnimation(null);
+        }
+        if (view instanceof ViewGroup) {
+            final ViewGroup viewGroup = (ViewGroup)view;
+            for (int index = 0; index < viewGroup.getChildCount(); index++) {
+                cancelRunningAnimations(viewGroup.getChildAt(index));
+            }
+        }
+    }
+
+    private ViewGroupAnimator(View rootView) {
+        mRootView = rootView;
+        mBeforeSnapshot = buildSnapshot(rootView);
+        cancelRunningAnimations(rootView);
+    }
+
+    /**
+     * Analyses the given view and its children, builds a snapshot and returns an animator
+     * that can later animate changes. This is the only function to get an instance of this class.
+     */
+    public static final ViewGroupAnimator captureView(View rootView) {
+       return new ViewGroupAnimator(rootView);
+    }
+
+    private void setVisibility(Iterable<View> views, int visibility) {
+        for (View view : views) view.setVisibility(visibility);
+    }
+
+    private void forceInstantRelayout() {
+        // This calls a framework internal function to instantly do the layout
+        // TODO: Find an officially supported way once the framework supports it
+
+        final ViewRoot vr = (ViewRoot) mRootView.getParent();
+        // vr can be null when rapidly chaining animations
+        if (vr != null) vr.handleMessage(Message.obtain(null, ViewRoot.DO_TRAVERSAL));
+    }
+
+    private void enableRedraw() {
+        mRootView.getViewTreeObserver().removeOnPreDrawListener(CANCEL_DRAW_LISTENER);
+    }
+
+    private void disableRedraw() {
+        mRootView.getViewTreeObserver().addOnPreDrawListener(CANCEL_DRAW_LISTENER);
+    }
+
+    /**
+     * Sets a function that should be called once all Animations are finished.
+     */
+    public void setOnAnimationsFinished(Runnable runnable) {
+        mOnAnimationsFinished = runnable;
+    }
+
+    /**
+     * Marks a view for deletion. This view will be set to both {@link View#INVISIBLE} and
+     * {@link View#GONE} during measurement and animation and will be removed from its Parent
+     * (using {@link ViewGroup#removeView(View)}) once all Animations are finished.
+     */
+    public void removeView(View view) {
+        mViewsToRemove.add(view);
+    }
+
+    /**
+     * Performs a difference analysis of positions and visibility, configures animations
+     * and starts them.
+     */
+    public void animate() {
+        disableRedraw();
+        try {
+            setVisibility(mViewsToRemove, View.GONE);
+            forceInstantRelayout();
+            final Snapshot currentSnapshot = buildSnapshot(mRootView);
+            final ArrayList<CachedTranslation> translations = new ArrayList<CachedTranslation>();
+            final HashSet<View> goneViews = new HashSet<View>();
+
+            AnimationManager animationManager = new AnimationManager();
+
+            for (String idChain : currentSnapshot.keySet()) {
+                final ViewInfo afterViewInfo = currentSnapshot.get(idChain);
+
+                if (mViewsToRemove.contains(afterViewInfo.getView())) {
+                    // There is special handling for these views below
+                    continue;
+                }
+
+                final ViewInfo beforeViewInfo = mBeforeSnapshot.get(idChain);
+
+                final boolean isVisible = afterViewInfo.getVisibility() == View.VISIBLE;
+
+                final boolean existedBefore = beforeViewInfo != null;
+                final boolean wasVisible = existedBefore &&
+                        beforeViewInfo.getVisibility() == View.VISIBLE;
+
+                if (isVisible && !wasVisible) {
+                    // this is a new View ==> fade it in
+                    animationManager.doFade(afterViewInfo.getView(),
+                            AnimationManager.FADE_TYPE_NEW);
+                    continue;
+                } else if (wasVisible && !isVisible) {
+                    if (afterViewInfo.getVisibility() == View.GONE) {
+                        goneViews.add(afterViewInfo.getView());
+                    } else {
+                        animationManager.doFade(afterViewInfo.getView(),
+                                AnimationManager.FADE_TYPE_VISIBLE_TO_INVISIBLE);
+                    }
+                    continue;
+                }
+
+                if (isVisible && wasVisible) {
+                    // Check if we have to Transform
+                    final Rect afterRectangle = afterViewInfo.getRectangle();
+                    final Rect beforeRectangle = beforeViewInfo.getRectangle();
+
+                    final int diffX = afterRectangle.left - beforeRectangle.left;
+                    final int diffY = afterRectangle.top - beforeRectangle.top;
+
+                    final boolean doTranslate = diffX != 0 || diffY != 0;
+
+                    if (doTranslate) {
+                        translations.add(new CachedTranslation(afterViewInfo.getView(),
+                                afterRectangle, diffX, diffY));
+                        continue;
+                    }
+                }
+            }
+
+            // Set views to Invisible, because we need their space for the layout
+            setVisibility(mViewsToRemove, View.INVISIBLE);
+            setVisibility(goneViews, View.INVISIBLE);
+            forceInstantRelayout();
+
+            for (CachedTranslation translation : translations) {
+                final Rect intermediatePosition = translation.getIntermediatePosition();
+                final int addX = intermediatePosition.left - translation.getView().getLeft();
+                final int addY = intermediatePosition.top - translation.getView().getTop();
+                animationManager.doTranslation(translation.getView(),
+                        addX - translation.getDiffX(), addX,
+                        addY - translation.getDiffY(), addY);
+            }
+
+            for (final View view : mViewsToRemove) {
+                animationManager.doFade(view, AnimationManager.FADE_TYPE_VISIBLE_TO_REMOVED);
+            }
+
+            for (final View view : goneViews) {
+                animationManager.doFade(view, AnimationManager.FADE_TYPE_VISIBLE_TO_GONE);
+            }
+        } finally {
+            enableRedraw();
+        }
+    }
+
+    private static Snapshot buildSnapshot(View rootView) {
+        final Snapshot result = new Snapshot();
+        buildSnapshotRecursive(rootView, result, "");
+        return result;
+    }
+
+    private static void buildSnapshotRecursive(View parentView,
+            Snapshot targetSnapshot, final String parentIdChain) {
+        if (!(parentView instanceof ViewGroup)) return;
+
+        final ViewGroup parentViewGroup = (ViewGroup) parentView;
+        for (int index = 0; index < parentViewGroup.getChildCount(); index++) {
+            final View view = parentViewGroup.getChildAt(index);
+            final int id = view.getId();
+            final String idChain;
+            if (id != View.NO_ID) {
+                idChain = parentIdChain + "/" + id;
+            } else {
+                idChain = parentIdChain + "/i" + index;
+            }
+
+            targetSnapshot.put(idChain, new ViewInfo(view));
+
+            buildSnapshotRecursive(view, targetSnapshot, idChain);
+        }
+    }
+
+    private final class AnimationManager implements AnimationListener {
+        private int mCountCalled = 0;
+
+        private static final int FADE_TYPE_VISIBLE_TO_GONE = 1;
+        private static final int FADE_TYPE_VISIBLE_TO_INVISIBLE = 2;
+        private static final int FADE_TYPE_VISIBLE_TO_REMOVED = 3;
+        private static final int FADE_TYPE_NEW = 4;
+
+        private static final int CLEANUP_NO_ACTION = 0;
+        private static final int CLEANUP_CLEAR_ANIMATION = 1;
+        private static final int CLEANUP_REMOVE = 2;
+        private static final int CLEANUP_SET_TO_GONE = 3;
+
+        private final HashMap<View, AnimationInfo> mAnimations = new HashMap<View, AnimationInfo>();
+
+        public void doTranslation(View view, int fromX, int toX, int fromY, int toY) {
+            final TranslateAnimation animation = new TranslateAnimation(
+                    fromX,
+                    toX,
+                    fromY,
+                    toY);
+            animation.setFillBefore(true);
+            animation.setFillAfter(true);
+            animation.setFillEnabled(true);
+            animation.setDuration(MOVE_DURATION_MILLIS_DEFAULT);
+            animation.setStartOffset(MOVE_OFFSET_MILLIS_DEFAULT);
+            animation.setInterpolator(INTERPOLATOR);
+            animation.setAnimationListener(this);
+
+            view.startAnimation(animation);
+
+            mAnimations.put(view, new AnimationInfo(animation, CLEANUP_CLEAR_ANIMATION));
+        }
+
+        public void doFade(View view, int fadeType) {
+            final float fromAlpha = fadeType == FADE_TYPE_NEW ? 0.0f : 1.0f;
+            final float toAlpha = fadeType == FADE_TYPE_NEW ? 1.0f : 0.0f;
+            final AlphaAnimation animation = new AlphaAnimation(fromAlpha, toAlpha);
+            animation.setDuration(FADE_DURATION_MILLIS_DEFAULT);
+            animation.setInterpolator(INTERPOLATOR);
+            animation.setAnimationListener(this);
+
+            final int cleanUpAction;
+            final boolean fill;
+            switch (fadeType) {
+                case FADE_TYPE_NEW:
+                    // No clean up necessary
+                    cleanUpAction = CLEANUP_NO_ACTION;
+                    fill = false;
+                    animation.setStartOffset(FADE_IN_OFFSET_MILLIS_DEFAULT);
+                    break;
+                case FADE_TYPE_VISIBLE_TO_GONE:
+                    cleanUpAction = CLEANUP_SET_TO_GONE;
+                    fill = true;
+                    animation.setStartOffset(FADE_OUT_OFFSET_MILLIS_DEFAULT);
+                    break;
+                case FADE_TYPE_VISIBLE_TO_INVISIBLE:
+                    // No clean up necessary
+                    cleanUpAction = CLEANUP_NO_ACTION;
+                    fill = false;
+                    animation.setStartOffset(FADE_OUT_OFFSET_MILLIS_DEFAULT);
+                    break;
+                case FADE_TYPE_VISIBLE_TO_REMOVED:
+                    cleanUpAction = CLEANUP_REMOVE;
+                    fill = true;
+                    animation.setStartOffset(FADE_OUT_OFFSET_MILLIS_DEFAULT);
+                    break;
+                default:
+                    throw new IllegalArgumentException("Unknown fadeType");
+            }
+            if (fill) {
+                animation.setFillBefore(true);
+                animation.setFillAfter(true);
+                animation.setFillEnabled(true);
+            }
+            mAnimations.put(view, new AnimationInfo(animation, cleanUpAction));
+
+            view.startAnimation(animation);
+        }
+
+        public void onAnimationEnd(Animation animation) {
+            mCountCalled++;
+            if (mCountCalled == mAnimations.size()) {
+                Log.d(TAG, "Cleaning up animations");
+
+                cleanUp();
+
+                if (mOnAnimationsFinished != null) mOnAnimationsFinished.run();
+            }
+        }
+
+        private void cleanUp() {
+            for (final View view : mAnimations.keySet()) {
+                final AnimationInfo animationInfo = mAnimations.get(view);
+                switch (animationInfo.getCleanUpAction()) {
+                    case CLEANUP_NO_ACTION:
+                    case CLEANUP_CLEAR_ANIMATION:
+                        if (view.getAnimation() != animationInfo.getAnimation()) continue;
+                        view.clearAnimation();
+                        break;
+                    case CLEANUP_REMOVE:
+                        final ViewGroup parentGroup = (ViewGroup) view.getParent();
+                        // has this view already been removed before?
+                        if (parentGroup != null) parentGroup.removeView(view);
+                        break;
+                    case CLEANUP_SET_TO_GONE:
+                        if (view.getAnimation() != animationInfo.getAnimation()) continue;
+                        view.clearAnimation();
+                        view.setVisibility(View.GONE);
+                        break;
+                    default:
+                        throw new IllegalStateException("Unknown cleanup type");
+                }
+            }
+        }
+
+        public void onAnimationRepeat(Animation animation) {
+        }
+
+        public void onAnimationStart(Animation animation) {
+        }
+    }
+
+    private final static class AnimationInfo {
+        private final Animation mAnimation;
+        private final int mCleanUpAction;
+
+        public Animation getAnimation() {
+            return mAnimation;
+        }
+        public int getCleanUpAction() {
+            return mCleanUpAction;
+        }
+
+        public AnimationInfo(Animation animation, int cleanUpAction) {
+            mAnimation = animation;
+            mCleanUpAction = cleanUpAction;
+        }
+    }
+
+    private final static class ViewInfo {
+        private final View mView;
+        private final Rect mRectangle;
+        private final int mVisibility;
+
+        public Rect getRectangle() {
+            return mRectangle;
+        }
+        public View getView() {
+            return mView;
+        }
+        public int getVisibility() {
+            return mVisibility;
+        }
+
+        public ViewInfo(View view) {
+            mView = view;
+            mRectangle = new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
+            mVisibility = view.getVisibility();
+        }
+    }
+
+    /**
+     * Shortcut to HashMap<String, ViewInfo>
+     */
+    private final static class Snapshot extends HashMap<String, ViewInfo> {
+
+    }
+
+    private final static class CachedTranslation {
+        private final View mView;
+        private final Rect mIntermediatePosition;
+        private final int mDiffX;
+        private final int mDiffY;
+
+        public View getView() {
+            return mView;
+        }
+
+        public Rect getIntermediatePosition() {
+            return mIntermediatePosition;
+        }
+
+        public int getDiffX() {
+            return mDiffX;
+        }
+
+        public int getDiffY() {
+            return mDiffY;
+        }
+
+        public CachedTranslation(View view, Rect intermediatePosition, int diffX, int diffY) {
+            mView = view;
+            mIntermediatePosition = intermediatePosition;
+            mDiffX = diffX;
+            mDiffY = diffY;
+        }
+    }
+}
diff --git a/src/com/android/contacts/views/detail/ContactDetailFragment.java b/src/com/android/contacts/views/detail/ContactDetailFragment.java
new file mode 100644
index 0000000..b4e74d3
--- /dev/null
+++ b/src/com/android/contacts/views/detail/ContactDetailFragment.java
@@ -0,0 +1,1032 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.views.detail;
+
+import com.android.contacts.Collapser;
+import com.android.contacts.ContactEntryAdapter;
+import com.android.contacts.ContactOptionsActivity;
+import com.android.contacts.ContactPresenceIconUtil;
+import com.android.contacts.ContactsUtils;
+import com.android.contacts.R;
+import com.android.contacts.TypePrecedence;
+import com.android.contacts.Collapser.Collapsible;
+import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.Sources;
+import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.util.Constants;
+import com.android.contacts.util.DataStatus;
+import com.android.internal.telephony.ITelephony;
+import com.android.internal.widget.ContactHeaderWidget;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.patterns.Loader;
+import android.app.patterns.LoaderManagingFragment;
+import android.content.ActivityNotFoundException;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Entity;
+import android.content.Intent;
+import android.content.Entity.NamedContentValues;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.net.ParseException;
+import android.net.Uri;
+import android.net.WebAddress;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.DisplayNameSources;
+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.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.View.OnClickListener;
+import android.view.View.OnCreateContextMenuListener;
+import android.widget.AdapterView;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.AdapterView.OnItemClickListener;
+
+import java.util.ArrayList;
+
+public class ContactDetailFragment extends LoaderManagingFragment<ContactDetailLoader.Result>
+        implements OnCreateContextMenuListener, OnItemClickListener {
+    private static final String TAG = "ContactDetailsView";
+    private static final boolean SHOW_SEPARATORS = false;
+
+    private static final int MENU_ITEM_MAKE_DEFAULT = 3;
+
+    private static final int LOADER_DETAILS = 1;
+
+    private Context mContext;
+    private Uri mLookupUri;
+    private Callbacks mCallbacks;
+
+    private ContactDetailLoader.Result mContactData;
+    private ContactHeaderWidget mContactHeaderWidget;
+    private ListView mListView;
+    private boolean mShowSmsLinksForAllPhones;
+    private ViewAdapter mAdapter;
+    private Uri mPrimaryPhoneUri = null;
+
+    private int mReadOnlySourcesCnt;
+    private int mWritableSourcesCnt;
+    private boolean mAllRestricted;
+    private final ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>();
+    private int mNumPhoneNumbers = 0;
+
+    /**
+     * The view shown if the detail list is empty.
+     * We set this to the list view when first bind the adapter, so that it won't be shown while
+     * we're loading data.
+     */
+    private View mEmptyView;
+
+    /**
+     * A list of distinct contact IDs included in the current contact.
+     */
+    private ArrayList<Long> mRawContactIds = new ArrayList<Long>();
+    private ArrayList<ViewEntry> mPhoneEntries = new ArrayList<ViewEntry>();
+    private ArrayList<ViewEntry> mSmsEntries = new ArrayList<ViewEntry>();
+    private ArrayList<ViewEntry> mEmailEntries = new ArrayList<ViewEntry>();
+    private ArrayList<ViewEntry> mPostalEntries = new ArrayList<ViewEntry>();
+    private ArrayList<ViewEntry> mImEntries = new ArrayList<ViewEntry>();
+    private ArrayList<ViewEntry> mNicknameEntries = new ArrayList<ViewEntry>();
+    private ArrayList<ViewEntry> mOrganizationEntries = new ArrayList<ViewEntry>();
+    private ArrayList<ViewEntry> mGroupEntries = new ArrayList<ViewEntry>();
+    private ArrayList<ViewEntry> mOtherEntries = new ArrayList<ViewEntry>();
+    private ArrayList<ArrayList<ViewEntry>> mSections = new ArrayList<ArrayList<ViewEntry>>();
+
+    public ContactDetailFragment() {
+        // Explicit constructor for inflation
+
+        // Build the list of sections. The order they're added to mSections dictates the
+        // order they are displayed in the list.
+        mSections.add(mPhoneEntries);
+        mSections.add(mSmsEntries);
+        mSections.add(mEmailEntries);
+        mSections.add(mImEntries);
+        mSections.add(mPostalEntries);
+        mSections.add(mNicknameEntries);
+        mSections.add(mOrganizationEntries);
+        mSections.add(mGroupEntries);
+        mSections.add(mOtherEntries);
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+        mContext = activity;
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
+        final View view = inflater.inflate(R.layout.contact_detail, container, false);
+
+        mContactHeaderWidget = (ContactHeaderWidget) view.findViewById(R.id.contact_header_widget);
+        mContactHeaderWidget.showStar(true);
+        mContactHeaderWidget.setExcludeMimes(new String[] {
+            Contacts.CONTENT_ITEM_TYPE
+        });
+
+        mListView = (ListView) view.findViewById(android.R.id.list);
+        mListView.setOnCreateContextMenuListener(this);
+        mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
+        mListView.setOnItemClickListener(this);
+        // Don't set it to mListView yet.  We do so later when we bind the adapter.
+        mEmptyView = view.findViewById(android.R.id.empty);
+
+        //TODO Read this value from a preference
+        mShowSmsLinksForAllPhones = true;
+
+        return view;
+    }
+
+    public void setCallbacks(Callbacks value) {
+        mCallbacks = value;
+    }
+
+    public void loadUri(Uri lookupUri) {
+        mLookupUri = lookupUri;
+        super.startLoading(LOADER_DETAILS, null);
+    }
+
+    @Override
+    protected void onInitializeLoaders() {
+//        startLoading(LOADER_DETAILS, null);
+    }
+
+    @Override
+    protected Loader<ContactDetailLoader.Result> onCreateLoader(int id, Bundle args) {
+        switch (id) {
+            case LOADER_DETAILS: {
+                return new ContactDetailLoader(mContext, mLookupUri);
+            }
+            default: {
+                Log.wtf(TAG, "Unknown ID in onCreateLoader: " + id);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    protected void onLoadFinished(Loader<ContactDetailLoader.Result> loader,
+            ContactDetailLoader.Result data) {
+        final int id = loader.getId();
+        switch (id) {
+            case LOADER_DETAILS:
+                if (data == ContactDetailLoader.Result.NOT_FOUND) {
+                    // Item has been deleted
+                    Log.i(TAG, "No contact found. Closing activity");
+                    mCallbacks.closeBecauseContactNotFound();
+                    return;
+                }
+                mContactData = data;
+                bindData();
+                break;
+            default: {
+                Log.wtf(TAG, "Unknown ID in onLoadFinished: " + id);
+            }
+        }
+    }
+
+    private void bindData() {
+        // Set the header
+        mContactHeaderWidget.setContactUri(mContactData.getLookupUri());
+        mContactHeaderWidget.setDisplayName(mContactData.getDisplayName(),
+                mContactData.getPhoneticName());
+        mContactHeaderWidget.setPhotoId(mContactData.getPhotoId(), mContactData.getLookupUri());
+        mContactHeaderWidget.setStared(mContactData.getStarred());
+        mContactHeaderWidget.setPresence(mContactData.getPresence());
+        mContactHeaderWidget.setStatus(
+                mContactData.getStatus(), mContactData.getStatusTimestamp(),
+                mContactData.getStatusLabel(), mContactData.getStatusResPackage());
+
+        // Build up the contact entries
+        buildEntries();
+
+        // Collapse similar data items in select sections.
+        Collapser.collapseList(mPhoneEntries);
+        Collapser.collapseList(mSmsEntries);
+        Collapser.collapseList(mEmailEntries);
+        Collapser.collapseList(mPostalEntries);
+        Collapser.collapseList(mImEntries);
+
+        if (mAdapter == null) {
+            mAdapter = new ViewAdapter(mContext, mSections);
+            mListView.setAdapter(mAdapter);
+        } else {
+            mAdapter.setSections(mSections, SHOW_SEPARATORS);
+        }
+        mListView.setEmptyView(mEmptyView);
+    }
+
+    /**
+     * Build up the entries to display on the screen.
+     */
+    private final void buildEntries() {
+        // Clear out the old entries
+        final int numSections = mSections.size();
+        for (int i = 0; i < numSections; i++) {
+            mSections.get(i).clear();
+        }
+
+        mRawContactIds.clear();
+
+        mReadOnlySourcesCnt = 0;
+        mWritableSourcesCnt = 0;
+        mAllRestricted = true;
+        mPrimaryPhoneUri = null;
+        mNumPhoneNumbers = 0;
+
+        mWritableRawContactIds.clear();
+
+        final Sources sources = Sources.getInstance(mContext);
+
+        // Build up method entries
+        if (mContactData == null) {
+            return;
+        }
+
+        for (Entity entity: mContactData.getEntities()) {
+            final ContentValues entValues = entity.getEntityValues();
+            final String accountType = entValues.getAsString(RawContacts.ACCOUNT_TYPE);
+            final long rawContactId = entValues.getAsLong(RawContacts._ID);
+
+            // Mark when this contact has any unrestricted components
+            final boolean isRestricted = entValues.getAsInteger(RawContacts.IS_RESTRICTED) != 0;
+            if (!isRestricted) mAllRestricted = false;
+
+            if (!mRawContactIds.contains(rawContactId)) {
+                mRawContactIds.add(rawContactId);
+            }
+            ContactsSource contactsSource = sources.getInflatedSource(accountType,
+                    ContactsSource.LEVEL_SUMMARY);
+            if (contactsSource != null && contactsSource.readOnly) {
+                mReadOnlySourcesCnt += 1;
+            } else {
+                mWritableSourcesCnt += 1;
+                mWritableRawContactIds.add(rawContactId);
+            }
+
+
+            for (NamedContentValues subValue : entity.getSubValues()) {
+                final ContentValues entryValues = subValue.values;
+                entryValues.put(Data.RAW_CONTACT_ID, rawContactId);
+
+                final long dataId = entryValues.getAsLong(Data._ID);
+                final String mimeType = entryValues.getAsString(Data.MIMETYPE);
+                if (mimeType == null) continue;
+
+                final DataKind kind = sources.getKindOrFallback(accountType, mimeType, mContext,
+                        ContactsSource.LEVEL_MIMETYPES);
+                if (kind == null) continue;
+
+                final ViewEntry entry = ViewEntry.fromValues(mContext, mimeType, kind,
+                        rawContactId, dataId, entryValues);
+
+                final boolean hasData = !TextUtils.isEmpty(entry.data);
+                final boolean isSuperPrimary = entryValues.getAsInteger(
+                        Data.IS_SUPER_PRIMARY) != 0;
+
+                if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+                    // Build phone entries
+                    mNumPhoneNumbers++;
+
+                    entry.intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
+                            Uri.fromParts(Constants.SCHEME_TEL, entry.data, null));
+                    entry.secondaryIntent = new Intent(Intent.ACTION_SENDTO,
+                            Uri.fromParts(Constants.SCHEME_SMSTO, entry.data, null));
+
+                    // Remember super-primary phone
+                    if (isSuperPrimary) mPrimaryPhoneUri = entry.uri;
+
+                    entry.isPrimary = isSuperPrimary;
+                    mPhoneEntries.add(entry);
+
+                    if (entry.type == CommonDataKinds.Phone.TYPE_MOBILE
+                            || mShowSmsLinksForAllPhones) {
+                        // Add an SMS entry
+                        if (kind.iconAltRes > 0) {
+                            entry.secondaryActionIcon = kind.iconAltRes;
+                        }
+                    }
+                } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+                    // Build email entries
+                    entry.intent = new Intent(Intent.ACTION_SENDTO,
+                            Uri.fromParts(Constants.SCHEME_MAILTO, entry.data, null));
+                    entry.isPrimary = isSuperPrimary;
+                    mEmailEntries.add(entry);
+
+                    // When Email rows have status, create additional Im row
+                    final DataStatus status = mContactData.getStatuses().get(entry.id);
+                    if (status != null) {
+                        final String imMime = Im.CONTENT_ITEM_TYPE;
+                        final DataKind imKind = sources.getKindOrFallback(accountType,
+                                imMime, mContext, ContactsSource.LEVEL_MIMETYPES);
+                        final ViewEntry imEntry = ViewEntry.fromValues(mContext,
+                                imMime, imKind, rawContactId, dataId, entryValues);
+                        imEntry.intent = ContactsUtils.buildImIntent(entryValues);
+                        imEntry.applyStatus(status, false);
+                        mImEntries.add(imEntry);
+                    }
+                } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+                    // Build postal entries
+                    entry.maxLines = 4;
+                    entry.intent = new Intent(Intent.ACTION_VIEW, entry.uri);
+                    mPostalEntries.add(entry);
+                } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+                    // Build IM entries
+                    entry.intent = ContactsUtils.buildImIntent(entryValues);
+                    if (TextUtils.isEmpty(entry.label)) {
+                        entry.label = mContext.getString(R.string.chat).toLowerCase();
+                    }
+
+                    // Apply presence and status details when available
+                    final DataStatus status = mContactData.getStatuses().get(entry.id);
+                    if (status != null) {
+                        entry.applyStatus(status, false);
+                    }
+                    mImEntries.add(entry);
+                } else if (Organization.CONTENT_ITEM_TYPE.equals(mimeType) &&
+                        (hasData || !TextUtils.isEmpty(entry.label))) {
+                    // Build organization entries
+                    final boolean isNameRawContact =
+                            (mContactData.getNameRawContactId() == rawContactId);
+
+                    final boolean duplicatesTitle =
+                            isNameRawContact
+                            && mContactData.getDisplayNameSource()
+                                == DisplayNameSources.ORGANIZATION
+                            && (!hasData || TextUtils.isEmpty(entry.label));
+
+                    if (!duplicatesTitle) {
+                        entry.uri = null;
+
+                        if (TextUtils.isEmpty(entry.label)) {
+                            entry.label = entry.data;
+                            entry.data = "";
+                        }
+
+                        mOrganizationEntries.add(entry);
+                    }
+                } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+                    // Build nickname entries
+                    final boolean isNameRawContact =
+                        (mContactData.getNameRawContactId() == rawContactId);
+
+                    final boolean duplicatesTitle =
+                        isNameRawContact
+                        && mContactData.getDisplayNameSource() == DisplayNameSources.NICKNAME;
+
+                    if (!duplicatesTitle) {
+                        entry.uri = null;
+                        mNicknameEntries.add(entry);
+                    }
+                } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+                    // Build note entries
+                    entry.uri = null;
+                    entry.maxLines = 100;
+                    mOtherEntries.add(entry);
+                } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+                    // Build note entries
+                    entry.uri = null;
+                    entry.maxLines = 10;
+                    try {
+                        WebAddress webAddress = new WebAddress(entry.data);
+                        entry.intent = new Intent(Intent.ACTION_VIEW,
+                                Uri.parse(webAddress.toString()));
+                    } catch (ParseException e) {
+                        Log.e(TAG, "Couldn't parse website: " + entry.data);
+                    }
+                    mOtherEntries.add(entry);
+                } else {
+                    // Handle showing custom rows
+                    entry.intent = new Intent(Intent.ACTION_VIEW, entry.uri);
+
+                    // Use social summary when requested by external source
+                    final DataStatus status = mContactData.getStatuses().get(entry.id);
+                    final boolean hasSocial = kind.actionBodySocial && status != null;
+                    if (hasSocial) {
+                        entry.applyStatus(status, true);
+                    }
+
+                    if (hasSocial || hasData) {
+                        mOtherEntries.add(entry);
+                    }
+                }
+            }
+        }
+    }
+
+    /* package */ static String buildActionString(DataKind kind, ContentValues values,
+            boolean lowerCase, Context context) {
+        if (kind.actionHeader == null) {
+            return null;
+        }
+        CharSequence actionHeader = kind.actionHeader.inflateUsing(context, values);
+        if (actionHeader == null) {
+            return null;
+        }
+        return lowerCase ? actionHeader.toString().toLowerCase() : actionHeader.toString();
+    }
+
+    /* package */ static String buildDataString(DataKind kind, ContentValues values,
+            Context context) {
+        if (kind.actionBody == null) {
+            return null;
+        }
+        CharSequence actionBody = kind.actionBody.inflateUsing(context, values);
+        return actionBody == null ? null : actionBody.toString();
+    }
+
+    /**
+     * A basic structure with the data for a contact entry in the list.
+     */
+    static class ViewEntry extends ContactEntryAdapter.Entry implements Collapsible<ViewEntry> {
+        public Context context = null;
+        public String resPackageName = null;
+        public int actionIcon = -1;
+        public boolean isPrimary = false;
+        public int secondaryActionIcon = -1;
+        public Intent intent;
+        public Intent secondaryIntent = null;
+        public int maxLabelLines = 1;
+        public ArrayList<Long> ids = new ArrayList<Long>();
+        public int collapseCount = 0;
+
+        public int presence = -1;
+
+        public CharSequence footerLine = null;
+
+        private ViewEntry() {
+        }
+
+        /**
+         * Build new {@link ViewEntry} and populate from the given values.
+         */
+        public static ViewEntry fromValues(Context context, String mimeType, DataKind kind,
+                long rawContactId, long dataId, ContentValues values) {
+            final ViewEntry entry = new ViewEntry();
+            entry.context = context;
+            entry.contactId = rawContactId;
+            entry.id = dataId;
+            entry.uri = ContentUris.withAppendedId(Data.CONTENT_URI, entry.id);
+            entry.mimetype = mimeType;
+            entry.label = buildActionString(kind, values, false, context);
+            entry.data = buildDataString(kind, values, context);
+
+            if (kind.typeColumn != null && values.containsKey(kind.typeColumn)) {
+                entry.type = values.getAsInteger(kind.typeColumn);
+            }
+            if (kind.iconRes > 0) {
+                entry.resPackageName = kind.resPackageName;
+                entry.actionIcon = kind.iconRes;
+            }
+
+            return entry;
+        }
+
+        /**
+         * Apply given {@link DataStatus} values over this {@link ViewEntry}
+         *
+         * @param fillData When true, the given status replaces {@link #data}
+         *            and {@link #footerLine}. Otherwise only {@link #presence}
+         *            is updated.
+         */
+        public ViewEntry applyStatus(DataStatus status, boolean fillData) {
+            presence = status.getPresence();
+            if (fillData && status.isValid()) {
+                this.data = status.getStatus().toString();
+                this.footerLine = status.getTimestampLabel(context);
+            }
+
+            return this;
+        }
+
+        public boolean collapseWith(ViewEntry entry) {
+            // assert equal collapse keys
+            if (!shouldCollapseWith(entry)) {
+                return false;
+            }
+
+            // Choose the label associated with the highest type precedence.
+            if (TypePrecedence.getTypePrecedence(mimetype, type)
+                    > TypePrecedence.getTypePrecedence(entry.mimetype, entry.type)) {
+                type = entry.type;
+                label = entry.label;
+            }
+
+            // Choose the max of the maxLines and maxLabelLines values.
+            maxLines = Math.max(maxLines, entry.maxLines);
+            maxLabelLines = Math.max(maxLabelLines, entry.maxLabelLines);
+
+            // Choose the presence with the highest precedence.
+            if (StatusUpdates.getPresencePrecedence(presence)
+                    < StatusUpdates.getPresencePrecedence(entry.presence)) {
+                presence = entry.presence;
+            }
+
+            // If any of the collapsed entries are primary make the whole thing primary.
+            isPrimary = entry.isPrimary ? true : isPrimary;
+
+            // uri, and contactdId, shouldn't make a difference. Just keep the original.
+
+            // Keep track of all the ids that have been collapsed with this one.
+            ids.add(entry.id);
+            collapseCount++;
+            return true;
+        }
+
+        public boolean shouldCollapseWith(ViewEntry entry) {
+            if (entry == null) {
+                return false;
+            }
+
+            if (!ContactsUtils.shouldCollapse(context, mimetype, data, entry.mimetype,
+                    entry.data)) {
+                return false;
+            }
+
+            if (!TextUtils.equals(mimetype, entry.mimetype)
+                    || !ContactsUtils.areIntentActionEqual(intent, entry.intent)
+                    || !ContactsUtils.areIntentActionEqual(secondaryIntent, entry.secondaryIntent)
+                    || actionIcon != entry.actionIcon) {
+                return false;
+            }
+
+            return true;
+        }
+    }
+
+    /** Cache of the children views of a row */
+    private static class ViewCache {
+        public TextView label;
+        public TextView data;
+        public TextView footer;
+        public ImageView actionIcon;
+        public ImageView presenceIcon;
+        public ImageView primaryIcon;
+        public ImageView secondaryActionButton;
+        public View secondaryActionDivider;
+
+        // Need to keep track of this too
+        public ViewEntry entry;
+    }
+
+    final class ViewAdapter extends ContactEntryAdapter<ViewEntry> implements OnClickListener {
+        ViewAdapter(Context context, ArrayList<ArrayList<ViewEntry>> sections) {
+            super(context, sections, SHOW_SEPARATORS);
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            final ViewEntry entry = getEntry(mSections, position, false);
+            final View v;
+            final ViewCache viewCache;
+
+            // Check to see if we can reuse convertView
+            if (convertView != null) {
+                v = convertView;
+                viewCache = (ViewCache) v.getTag();
+            } else {
+                // Create a new view if needed
+                v = mInflater.inflate(R.layout.list_item_text_icons, parent, false);
+
+                // Cache the children
+                viewCache = new ViewCache();
+                viewCache.label = (TextView) v.findViewById(android.R.id.text1);
+                viewCache.data = (TextView) v.findViewById(android.R.id.text2);
+                viewCache.footer = (TextView) v.findViewById(R.id.footer);
+                viewCache.actionIcon = (ImageView) v.findViewById(R.id.action_icon);
+                viewCache.primaryIcon = (ImageView) v.findViewById(R.id.primary_icon);
+                viewCache.presenceIcon = (ImageView) v.findViewById(R.id.presence_icon);
+                viewCache.secondaryActionButton = (ImageView) v.findViewById(
+                        R.id.secondary_action_button);
+                viewCache.secondaryActionButton.setOnClickListener(this);
+                viewCache.secondaryActionDivider = v.findViewById(R.id.divider);
+                v.setTag(viewCache);
+            }
+
+            // Update the entry in the view cache
+            viewCache.entry = entry;
+
+            // Bind the data to the view
+            bindView(v, entry);
+            return v;
+        }
+
+        @Override
+        protected View newView(int position, ViewGroup parent) {
+            // getView() handles this
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        protected void bindView(View view, ViewEntry entry) {
+            final Resources resources = mContext.getResources();
+            ViewCache views = (ViewCache) view.getTag();
+
+            // Set the label
+            TextView label = views.label;
+            setMaxLines(label, entry.maxLabelLines);
+            label.setText(entry.label);
+
+            // Set the data
+            TextView data = views.data;
+            if (data != null) {
+                if (entry.mimetype.equals(Phone.CONTENT_ITEM_TYPE)
+                        || entry.mimetype.equals(Constants.MIME_SMS_ADDRESS)) {
+                    data.setText(PhoneNumberUtils.formatNumber(entry.data));
+                } else {
+                    data.setText(entry.data);
+                }
+                setMaxLines(data, entry.maxLines);
+            }
+
+            // Set the footer
+            if (!TextUtils.isEmpty(entry.footerLine)) {
+                views.footer.setText(entry.footerLine);
+                views.footer.setVisibility(View.VISIBLE);
+            } else {
+                views.footer.setVisibility(View.GONE);
+            }
+
+            // Set the primary icon
+            views.primaryIcon.setVisibility(entry.isPrimary ? View.VISIBLE : View.GONE);
+
+            // Set the action icon
+            ImageView action = views.actionIcon;
+            if (entry.actionIcon != -1) {
+                Drawable actionIcon;
+                if (entry.resPackageName != null) {
+                    // Load external resources through PackageManager
+                    actionIcon = mContext.getPackageManager().getDrawable(entry.resPackageName,
+                            entry.actionIcon, null);
+                } else {
+                    actionIcon = resources.getDrawable(entry.actionIcon);
+                }
+                action.setImageDrawable(actionIcon);
+                action.setVisibility(View.VISIBLE);
+            } else {
+                // Things should still line up as if there was an icon, so make it invisible
+                action.setVisibility(View.INVISIBLE);
+            }
+
+            // Set the presence icon
+            Drawable presenceIcon = ContactPresenceIconUtil.getPresenceIcon(
+                    mContext, entry.presence);
+            ImageView presenceIconView = views.presenceIcon;
+            if (presenceIcon != null) {
+                presenceIconView.setImageDrawable(presenceIcon);
+                presenceIconView.setVisibility(View.VISIBLE);
+            } else {
+                presenceIconView.setVisibility(View.GONE);
+            }
+
+            // Set the secondary action button
+            ImageView secondaryActionView = views.secondaryActionButton;
+            Drawable secondaryActionIcon = null;
+            if (entry.secondaryActionIcon != -1) {
+                secondaryActionIcon = resources.getDrawable(entry.secondaryActionIcon);
+            }
+            if (entry.secondaryIntent != null && secondaryActionIcon != null) {
+                secondaryActionView.setImageDrawable(secondaryActionIcon);
+                secondaryActionView.setTag(entry);
+                secondaryActionView.setVisibility(View.VISIBLE);
+                views.secondaryActionDivider.setVisibility(View.VISIBLE);
+            } else {
+                secondaryActionView.setVisibility(View.GONE);
+                views.secondaryActionDivider.setVisibility(View.GONE);
+            }
+        }
+
+        private void setMaxLines(TextView textView, int maxLines) {
+            if (maxLines == 1) {
+                textView.setSingleLine(true);
+                textView.setEllipsize(TextUtils.TruncateAt.END);
+            } else {
+                textView.setSingleLine(false);
+                textView.setMaxLines(maxLines);
+                textView.setEllipsize(null);
+            }
+        }
+
+        public void onClick(View v) {
+            if (mCallbacks == null) return;
+            if (v == null) return;
+            final ViewEntry entry = (ViewEntry) v.getTag();
+            if (entry == null) return;
+            final Intent intent = entry.secondaryIntent;
+            if (intent == null) return;
+            mCallbacks.itemClicked(intent);
+        }
+    }
+
+    public boolean onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
+        inflater.inflate(R.menu.view, menu);
+        return true;
+    }
+
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        // Only allow edit when we have at least one raw_contact id
+        final boolean hasRawContact = (mRawContactIds.size() > 0);
+        menu.findItem(R.id.menu_edit).setEnabled(hasRawContact);
+
+        // Only allow share when unrestricted contacts available
+        menu.findItem(R.id.menu_share).setEnabled(!mAllRestricted);
+
+        return true;
+    }
+
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.menu_edit: {
+                if (mRawContactIds.size() > 0) {
+                    final long rawContactIdToEdit = mRawContactIds.get(0);
+                    final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
+                            rawContactIdToEdit);
+                    mCallbacks.editContact(rawContactUri);
+                    return true;
+                } else {
+                    // There is no rawContact to edit.
+                    return false;
+                }
+            }
+            case R.id.menu_delete: {
+                showDeleteConfirmationDialog();
+                return true;
+            }
+            case R.id.menu_options: {
+                final Intent intent = new Intent(mContext, ContactOptionsActivity.class);
+                intent.setData(mContactData.getLookupUri());
+                mContext.startActivity(intent);
+                return true;
+            }
+            case R.id.menu_share: {
+                if (mAllRestricted) return false;
+
+                final String lookupKey = mContactData.getLookupKey();
+                final Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey);
+
+                final Intent intent = new Intent(Intent.ACTION_SEND);
+                intent.setType(Contacts.CONTENT_VCARD_TYPE);
+                intent.putExtra(Intent.EXTRA_STREAM, shareUri);
+
+                // Launch chooser to share contact via
+                final CharSequence chooseTitle = mContext.getText(R.string.share_via);
+                final Intent chooseIntent = Intent.createChooser(intent, chooseTitle);
+
+                try {
+                    mContext.startActivity(chooseIntent);
+                } catch (ActivityNotFoundException ex) {
+                    Toast.makeText(mContext, R.string.share_error, Toast.LENGTH_SHORT).show();
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void showDeleteConfirmationDialog() {
+        if (mReadOnlySourcesCnt > 0 & mWritableSourcesCnt > 0) {
+            getActivity().showDialog(R.id.detail_dialog_confirm_readonly_delete);
+        } else if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt == 0) {
+            getActivity().showDialog(R.id.detail_dialog_confirm_readonly_hide);
+        } else if (mReadOnlySourcesCnt == 0 && mWritableSourcesCnt > 1) {
+            getActivity().showDialog(R.id.detail_dialog_confirm_multiple_delete);
+        } else {
+            getActivity().showDialog(R.id.detail_dialog_confirm_delete);
+        }
+    }
+
+    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+        AdapterView.AdapterContextMenuInfo info;
+        try {
+             info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+        } catch (ClassCastException e) {
+            Log.e(TAG, "bad menuInfo", e);
+            return;
+        }
+
+        // This can be null sometimes, don't crash...
+        if (info == null) {
+            Log.e(TAG, "bad menuInfo");
+            return;
+        }
+
+        ViewEntry entry = ContactEntryAdapter.getEntry(mSections, info.position, SHOW_SEPARATORS);
+        menu.setHeaderTitle(R.string.contactOptionsTitle);
+        if (entry.mimetype.equals(CommonDataKinds.Phone.CONTENT_ITEM_TYPE)) {
+            menu.add(0, 0, 0, R.string.menu_call).setIntent(entry.intent);
+            menu.add(0, 0, 0, R.string.menu_sendSMS).setIntent(entry.secondaryIntent);
+            if (!entry.isPrimary) {
+                menu.add(0, MENU_ITEM_MAKE_DEFAULT, 0, R.string.menu_makeDefaultNumber);
+            }
+        } else if (entry.mimetype.equals(CommonDataKinds.Email.CONTENT_ITEM_TYPE)) {
+            menu.add(0, 0, 0, R.string.menu_sendEmail).setIntent(entry.intent);
+            if (!entry.isPrimary) {
+                menu.add(0, MENU_ITEM_MAKE_DEFAULT, 0, R.string.menu_makeDefaultEmail);
+            }
+        } else if (entry.mimetype.equals(CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE)) {
+            menu.add(0, 0, 0, R.string.menu_viewAddress).setIntent(entry.intent);
+        }
+    }
+
+    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+        if (mCallbacks == null) return;
+        final ViewEntry entry = ViewAdapter.getEntry(mSections, position, SHOW_SEPARATORS);
+        if (entry == null) return;
+        final Intent intent = entry.intent;
+        if (intent == null) return;
+        mCallbacks.itemClicked(intent);
+    }
+
+    private final DialogInterface.OnClickListener mDeleteListener =
+            new DialogInterface.OnClickListener() {
+        public void onClick(DialogInterface dialog, int which) {
+            mContext.getContentResolver().delete(mContactData.getLookupUri(), null, null);
+        }
+    };
+
+    public Dialog onCreateDialog(int id, Bundle bundle) {
+        switch (id) {
+            case R.id.detail_dialog_confirm_delete:
+                return new AlertDialog.Builder(mContext)
+                        .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, mDeleteListener)
+                        .setCancelable(false)
+                        .create();
+            case R.id.detail_dialog_confirm_readonly_delete:
+                return new AlertDialog.Builder(mContext)
+                        .setTitle(R.string.deleteConfirmation_title)
+                        .setIcon(android.R.drawable.ic_dialog_alert)
+                        .setMessage(R.string.readOnlyContactDeleteConfirmation)
+                        .setNegativeButton(android.R.string.cancel, null)
+                        .setPositiveButton(android.R.string.ok, mDeleteListener)
+                        .setCancelable(false)
+                        .create();
+            case R.id.detail_dialog_confirm_multiple_delete:
+                return new AlertDialog.Builder(mContext)
+                        .setTitle(R.string.deleteConfirmation_title)
+                        .setIcon(android.R.drawable.ic_dialog_alert)
+                        .setMessage(R.string.multipleContactDeleteConfirmation)
+                        .setNegativeButton(android.R.string.cancel, null)
+                        .setPositiveButton(android.R.string.ok, mDeleteListener)
+                        .setCancelable(false)
+                        .create();
+            case R.id.detail_dialog_confirm_readonly_hide: {
+                return new AlertDialog.Builder(mContext)
+                        .setTitle(R.string.deleteConfirmation_title)
+                        .setIcon(android.R.drawable.ic_dialog_alert)
+                        .setMessage(R.string.readOnlyContactWarning)
+                        .setPositiveButton(android.R.string.ok, mDeleteListener)
+                        .create();
+            }
+            default:
+                return null;
+        }
+    }
+
+    public boolean onContextItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case MENU_ITEM_MAKE_DEFAULT: {
+                if (makeItemDefault(item)) {
+                    return true;
+                }
+                break;
+            }
+        }
+
+        return false;
+    }
+
+    private boolean makeItemDefault(MenuItem item) {
+        ViewEntry entry = getViewEntryForMenuItem(item);
+        if (entry == null) {
+            return false;
+        }
+
+        // Update the primary values in the data record.
+        ContentValues values = new ContentValues(1);
+        values.put(Data.IS_SUPER_PRIMARY, 1);
+
+        mContext.getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, entry.id),
+                values, null, null);
+        return true;
+    }
+
+    private ViewEntry getViewEntryForMenuItem(MenuItem item) {
+        AdapterView.AdapterContextMenuInfo info;
+        try {
+             info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
+        } catch (ClassCastException e) {
+            Log.e(TAG, "bad menuInfo", e);
+            return null;
+        }
+
+        return ContactEntryAdapter.getEntry(mSections, info.position, SHOW_SEPARATORS);
+    }
+
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_CALL: {
+                try {
+                    ITelephony phone = ITelephony.Stub.asInterface(
+                            ServiceManager.checkService("phone"));
+                    if (phone != null && !phone.isIdle()) {
+                        // Skip out and let the key be handled at a higher level
+                        break;
+                    }
+                } catch (RemoteException re) {
+                    // Fall through and try to call the contact
+                }
+
+                int index = mListView.getSelectedItemPosition();
+                if (index != -1) {
+                    final ViewEntry entry = ViewAdapter.getEntry(mSections, index, SHOW_SEPARATORS);
+                    if (entry != null &&
+                            entry.intent.getAction() == Intent.ACTION_CALL_PRIVILEGED) {
+                        mContext.startActivity(entry.intent);
+                        return true;
+                    }
+                } else if (mPrimaryPhoneUri != null) {
+                    // There isn't anything selected, call the default number
+                    final Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
+                            mPrimaryPhoneUri);
+                    mContext.startActivity(intent);
+                    return true;
+                }
+                return false;
+            }
+
+            case KeyEvent.KEYCODE_DEL: {
+                showDeleteConfirmationDialog();
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    public static interface Callbacks {
+        /**
+         * Contact was not found, so somehow close this fragment.
+         */
+        public void closeBecauseContactNotFound();
+
+        /**
+         * User decided to go to Edit-Mode
+         */
+        public void editContact(Uri rawContactUri);
+
+        /**
+         * User clicked a single item (e.g. mail)
+         */
+        public void itemClicked(Intent intent);
+    }
+}
diff --git a/src/com/android/contacts/views/detail/ContactDetailLoader.java b/src/com/android/contacts/views/detail/ContactDetailLoader.java
new file mode 100644
index 0000000..147ec3c
--- /dev/null
+++ b/src/com/android/contacts/views/detail/ContactDetailLoader.java
@@ -0,0 +1,491 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.views.detail;
+
+import com.android.contacts.util.DataStatus;
+
+import android.app.patterns.Loader;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Entity;
+import android.content.EntityIterator;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.RawContactsEntity;
+import android.provider.ContactsContract.StatusUpdates;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Loads a single Contact and all it constituent RawContacts.
+ */
+public class ContactDetailLoader extends Loader<ContactDetailLoader.Result> {
+    private static final String TAG = "ContactLoader";
+
+    private Uri mLookupUri;
+    private Result mContact;
+    private ForceLoadContentObserver mObserver;
+    private boolean mDestroyed;
+
+    public interface Callbacks {
+        public void onContactLoaded(Result contact);
+    }
+
+    /**
+     * The result of a load operation. Contains all data necessary to display the contact.
+     */
+    public static final class Result {
+        /**
+         * Singleton instance that represents "No Contact Found"
+         */
+        public static final Result NOT_FOUND = new Result();
+
+        /**
+         * Singleton instance that represents an error, e.g. because of an invalid Uri
+         * TODO: We should come up with something nicer here. Maybe use an Either type so
+         * that we can capture the Exception?
+         */
+        public static final Result ERROR = new Result();
+
+        private final Uri mLookupUri;
+        private final String mLookupKey;
+        private final Uri mUri;
+        private final long mId;
+        private final long mNameRawContactId;
+        private final int mDisplayNameSource;
+        private final long mPhotoId;
+        private final String mDisplayName;
+        private final String mPhoneticName;
+        private final boolean mStarred;
+        private final Integer mPresence;
+        private final ArrayList<Entity> mEntities;
+        private final HashMap<Long, DataStatus> mStatuses;
+        private final String mStatus;
+        private final Long mStatusTimestamp;
+        private final Integer mStatusLabel;
+        private final String mStatusResPackage;
+
+        /**
+         * Constructor for case "no contact found". This must only be used for the
+         * final {@link Result#NOT_FOUND} singleton
+         */
+        private Result() {
+            mLookupUri = null;
+            mLookupKey = null;
+            mUri = null;
+            mId = -1;
+            mEntities = null;
+            mStatuses = null;
+            mNameRawContactId = -1;
+            mDisplayNameSource = DisplayNameSources.UNDEFINED;
+            mPhotoId = -1;
+            mDisplayName = null;
+            mPhoneticName = null;
+            mStarred = false;
+            mPresence = null;
+            mStatus = null;
+            mStatusTimestamp = null;
+            mStatusLabel = null;
+            mStatusResPackage = null;
+        }
+
+        /**
+         * Constructor to call when contact was found
+         */
+        private Result(Uri lookupUri, String lookupKey, Uri uri, long id, long nameRawContactId,
+                int displayNameSource, long photoId, String displayName, String phoneticName,
+                boolean starred, Integer presence, String status, Long statusTimestamp,
+                Integer statusLabel, String statusResPackage) {
+            mLookupUri = lookupUri;
+            mLookupKey = lookupKey;
+            mUri = uri;
+            mId = id;
+            mEntities = new ArrayList<Entity>();
+            mStatuses = new HashMap<Long, DataStatus>();
+            mNameRawContactId = nameRawContactId;
+            mDisplayNameSource = displayNameSource;
+            mPhotoId = photoId;
+            mDisplayName = displayName;
+            mPhoneticName = phoneticName;
+            mStarred = starred;
+            mPresence = presence;
+            mStatus = status;
+            mStatusTimestamp = statusTimestamp;
+            mStatusLabel = statusLabel;
+            mStatusResPackage = statusResPackage;
+        }
+
+        public Uri getLookupUri() {
+            return mLookupUri;
+        }
+        public String getLookupKey() {
+            return mLookupKey;
+        }
+        public Uri getUri() {
+            return mUri;
+        }
+        public long getId() {
+            return mId;
+        }
+        public long getNameRawContactId() {
+            return mNameRawContactId;
+        }
+        public int getDisplayNameSource() {
+            return mDisplayNameSource;
+        }
+        public long getPhotoId() {
+            return mPhotoId;
+        }
+        public String getDisplayName() {
+            return mDisplayName;
+        }
+        public String getPhoneticName() {
+            return mPhoneticName;
+        }
+        public boolean getStarred() {
+            return mStarred;
+        }
+        public Integer getPresence() {
+            return mPresence;
+        }
+        public String getStatus() {
+            return mStatus;
+        }
+        public Long getStatusTimestamp() {
+            return mStatusTimestamp;
+        }
+        public Integer getStatusLabel() {
+            return mStatusLabel;
+        }
+        public String getStatusResPackage() {
+            return mStatusResPackage;
+        }
+        public ArrayList<Entity> getEntities() {
+            return mEntities;
+        }
+        public HashMap<Long, DataStatus> getStatuses() {
+            return mStatuses;
+        }
+    }
+
+    private interface StatusQuery {
+        final String[] PROJECTION = new String[] {
+                Data._ID, Data.STATUS, Data.STATUS_RES_PACKAGE, Data.STATUS_ICON,
+                Data.STATUS_LABEL, Data.STATUS_TIMESTAMP, Data.PRESENCE,
+        };
+
+        final int _ID = 0;
+    }
+
+    private interface ContactQuery {
+        //Projection used for the summary info in the header.
+        final static String[] COLUMNS = new String[] {
+                Contacts.NAME_RAW_CONTACT_ID,
+                Contacts.DISPLAY_NAME_SOURCE,
+                Contacts.LOOKUP_KEY,
+                Contacts.DISPLAY_NAME,
+                Contacts.PHONETIC_NAME,
+                Contacts.PHOTO_ID,
+                Contacts.STARRED,
+                Contacts.CONTACT_PRESENCE,
+                Contacts.CONTACT_STATUS,
+                Contacts.CONTACT_STATUS_TIMESTAMP,
+                Contacts.CONTACT_STATUS_RES_PACKAGE,
+                Contacts.CONTACT_STATUS_LABEL,
+        };
+        final static int NAME_RAW_CONTACT_ID = 0;
+        final static int DISPLAY_NAME_SOURCE = 1;
+        final static int LOOKUP_KEY = 2;
+        final static int DISPLAY_NAME = 3;
+        final static int PHONETIC_NAME = 4;
+        final static int PHOTO_ID = 5;
+        final static int STARRED = 6;
+        final static int CONTACT_PRESENCE = 7;
+        final static int CONTACT_STATUS = 8;
+        final static int CONTACT_STATUS_TIMESTAMP = 9;
+        final static int CONTACT_STATUS_RES_PACKAGE = 10;
+        final static int CONTACT_STATUS_LABEL = 11;
+    }
+
+    private final class LoadContactTask extends AsyncTask<Void, Void, Result> {
+
+        @Override
+        protected Result doInBackground(Void... args) {
+            try {
+                final ContentResolver resolver = getContext().getContentResolver();
+                final Uri uriCurrentFormat = convertLegacyIfNecessary(mLookupUri);
+                Result result = loadContactHeaderData(resolver, uriCurrentFormat);
+                if (result == Result.NOT_FOUND) {
+                    // No record found. Try to lookup up a new record with the same lookupKey.
+                    // We might have went through a sync where Ids changed
+                    final Uri freshLookupUri = Contacts.getLookupUri(resolver, uriCurrentFormat);
+                    result = loadContactHeaderData(resolver, freshLookupUri);
+                    if (result == Result.NOT_FOUND) {
+                        // Still not found. We now believe this contact really does not exist
+                        Log.e(TAG, "invalid contact uri: " + mLookupUri);
+                        return Result.NOT_FOUND;
+                    }
+                }
+
+                // These queries could be run in parallel (we did this until froyo). But unless
+                // we actually have two database connections there is no performance gain
+                loadSocial(resolver, result);
+                loadRawContacts(resolver, result);
+
+                return result;
+            } catch (Exception e) {
+                return Result.ERROR;
+            }
+        }
+
+        /**
+         * Transforms the given Uri and returns a Lookup-Uri that represents the contact.
+         * For legacy contacts, a raw-contact lookup is performed.
+         */
+        private Uri convertLegacyIfNecessary(Uri uri) {
+            if (uri == null) throw new IllegalArgumentException("uri must not be null");
+
+            final String authority = uri.getAuthority();
+
+            // Current Style Uri? Just return it
+            if (ContactsContract.AUTHORITY.equals(authority)) {
+                return uri;
+            }
+
+            // Legacy Style? Convert to RawContact
+            final String OBSOLETE_AUTHORITY = "contacts";
+            if (OBSOLETE_AUTHORITY.equals(authority)) {
+                // Legacy Format. Convert to RawContact-Uri and then lookup the contact
+                final long rawContactId = ContentUris.parseId(uri);
+                return RawContacts.getContactLookupUri(getContext().getContentResolver(),
+                        ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
+            }
+
+            throw new IllegalArgumentException("uri format is unknown");
+        }
+
+        /**
+         * Tries to lookup a contact using both Id and lookup key of the given Uri. Returns a
+         * valid Result instance if successful or {@link Result#NOT_FOUND} if empty
+         */
+        private Result loadContactHeaderData(final ContentResolver resolver, final Uri lookupUri) {
+            if (resolver == null) throw new IllegalArgumentException("resolver must not be null");
+            if (lookupUri == null) {
+                // This can happen if the row was removed
+                return Result.NOT_FOUND;
+            }
+
+            final List<String> segments = lookupUri.getPathSegments();
+            if (segments.size() != 4) {
+                // Does not contain an Id. Return to caller so that a lookup is performed
+                Log.w(TAG, "Uri does not contain an Id, so we return to the caller who should " +
+                        "perform a lookup to get a proper uri. Value: " + lookupUri);
+                return Result.NOT_FOUND;
+            }
+
+            final long uriContactId = Long.parseLong(segments.get(3));
+            final String uriLookupKey = Uri.encode(segments.get(2));
+            final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, uriContactId);
+
+            final Cursor cursor = resolver.query(contactUri, ContactQuery.COLUMNS, null, null,
+                    null);
+            if (cursor == null) {
+                Log.e(TAG, "No cursor returned in trySetupContactHeader/query");
+                return null;
+            }
+            try {
+                if (!cursor.moveToFirst()) {
+                    Log.w(TAG, "Cursor returned by trySetupContactHeader/query is empty. " +
+                            "ContactId must have changed or item has been removed");
+                    return Result.NOT_FOUND;
+                }
+                final String lookupKey = cursor.getString(ContactQuery.LOOKUP_KEY);
+                if (!lookupKey.equals(uriLookupKey)) {
+                    // ID and lookup key do not match
+                    Log.w(TAG, "Contact with Id=" + uriContactId + " has a wrong lookupKey ("
+                            + lookupKey + " instead of the expected " + uriLookupKey + ")");
+                    return Result.NOT_FOUND;
+                }
+
+                final long nameRawContactId = cursor.getLong(ContactQuery.NAME_RAW_CONTACT_ID);
+                final int displayNameSource = cursor.getInt(ContactQuery.DISPLAY_NAME_SOURCE);
+                final String displayName = cursor.getString(ContactQuery.DISPLAY_NAME);
+                final String phoneticName = cursor.getString(ContactQuery.PHONETIC_NAME);
+                final long photoId = cursor.getLong(ContactQuery.PHOTO_ID);
+                final boolean starred = cursor.getInt(ContactQuery.STARRED) != 0;
+                final Integer presence = cursor.isNull(ContactQuery.CONTACT_PRESENCE)
+                        ? null
+                        : cursor.getInt(ContactQuery.CONTACT_PRESENCE);
+                final String status = cursor.getString(ContactQuery.CONTACT_STATUS);
+                final Long statusTimestamp = cursor.isNull(ContactQuery.CONTACT_STATUS_TIMESTAMP)
+                        ? null
+                        : cursor.getLong(ContactQuery.CONTACT_STATUS_TIMESTAMP);
+                final Integer statusLabel = cursor.isNull(ContactQuery.CONTACT_STATUS_LABEL)
+                        ? null
+                        : cursor.getInt(ContactQuery.CONTACT_STATUS_LABEL);
+                final String statusResPackage = cursor.getString(
+                        ContactQuery.CONTACT_STATUS_RES_PACKAGE);
+
+                return new Result(lookupUri, lookupKey, contactUri, uriContactId, nameRawContactId,
+                        displayNameSource, photoId, displayName, phoneticName, starred, presence,
+                        status, statusTimestamp, statusLabel, statusResPackage);
+            } finally {
+                cursor.close();
+            }
+        }
+
+        /**
+         * Loads the social rows into the result structure. Expects the statuses in the
+         * result structure to be empty
+         */
+        private void loadSocial(final ContentResolver resolver, final Result result) {
+            if (result == null) throw new IllegalArgumentException("result must not be null");
+            if (resolver == null) throw new IllegalArgumentException("resolver must not be null");
+            if (result == Result.NOT_FOUND) {
+                throw new IllegalArgumentException("result must not be NOT_FOUND");
+            }
+
+            final Uri dataUri = Uri.withAppendedPath(result.getUri(),
+                    Contacts.Data.CONTENT_DIRECTORY);
+            final Cursor cursor = resolver.query(dataUri, StatusQuery.PROJECTION,
+                    StatusUpdates.PRESENCE + " IS NOT NULL OR " + StatusUpdates.STATUS +
+                    " IS NOT NULL", null, null);
+
+            if (cursor == null) {
+                Log.e(TAG, "Social cursor is null but it shouldn't be");
+                return;
+            }
+
+            try {
+                HashMap<Long, DataStatus> statuses = result.getStatuses();
+
+                // Walk found statuses, creating internal row for each
+                while (cursor.moveToNext()) {
+                    final DataStatus status = new DataStatus(cursor);
+                    final long dataId = cursor.getLong(StatusQuery._ID);
+                    statuses.put(dataId, status);
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+
+        /**
+         * Loads the raw row contact rows into the result structure. Expects the entities in the
+         * result structure to be empty
+         */
+        private void loadRawContacts(final ContentResolver resolver, final Result result) {
+            if (result == null) throw new IllegalArgumentException("result must not be null");
+            if (resolver == null) throw new IllegalArgumentException("resolver must not be null");
+            if (result == Result.NOT_FOUND) {
+                throw new IllegalArgumentException("result must not be NOT_FOUND");
+            }
+
+            // Read the constituent raw contacts
+            final Cursor cursor = resolver.query(RawContactsEntity.CONTENT_URI, null,
+                    RawContacts.CONTACT_ID + "=?", new String[] {
+                            String.valueOf(result.mId)
+                    }, null);
+            if (cursor == null) {
+                Log.e(TAG, "Raw contacts cursor is null but it shouldn't be");
+                return;
+            }
+
+            try {
+                ArrayList<Entity> entities = result.getEntities();
+                entities.ensureCapacity(cursor.getCount());
+                EntityIterator iterator = RawContacts.newEntityIterator(cursor);
+                try {
+                    while (iterator.hasNext()) {
+                        Entity entity = iterator.next();
+                        entities.add(entity);
+                    }
+                } finally {
+                    iterator.close();
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+
+        @Override
+        protected void onPostExecute(Result result) {
+            // The creator isn't interested in any further updates
+            if (mDestroyed) {
+                return;
+            }
+
+            mContact = result;
+            if (result != null) {
+                if (mObserver == null) {
+                    mObserver = new ForceLoadContentObserver();
+                }
+                Log.i(TAG, "Registering content observer for " + mLookupUri);
+
+                if (result != Result.ERROR && result != Result.NOT_FOUND) {
+                    getContext().getContentResolver().registerContentObserver(mLookupUri, true,
+                            mObserver);
+                }
+                deliverResult(result);
+            }
+        }
+    }
+
+    public ContactDetailLoader(Context context, Uri lookupUri) {
+        super(context);
+        mLookupUri = lookupUri;
+    }
+
+    @Override
+    public void startLoading() {
+        if (mContact != null) {
+            deliverResult(mContact);
+        } else {
+            forceLoad();
+        }
+    }
+
+    @Override
+    public void forceLoad() {
+        final LoadContactTask task = new LoadContactTask();
+        task.execute((Void[])null);
+    }
+
+    @Override
+    public void stopLoading() {
+        mContact = null;
+        if (mObserver != null) {
+            getContext().getContentResolver().unregisterContentObserver(mObserver);
+        }
+    }
+
+    @Override
+    public void destroy() {
+        mContact = null;
+        mDestroyed = true;
+    }
+}
diff --git a/src/com/android/contacts/views/edit/ContactEditFragment.java b/src/com/android/contacts/views/edit/ContactEditFragment.java
new file mode 100644
index 0000000..a2629b1
--- /dev/null
+++ b/src/com/android/contacts/views/edit/ContactEditFragment.java
@@ -0,0 +1,1326 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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
+ */
+
+
+// Here are the open TODOs for the Fragment transition
+// TODO How to save data? Service?
+// TODO Do account-list lookup always in a thread
+// TODO Remove the temporary instance once findFragmentById works
+// TODO Cleanup state handling (orientation changes etc).
+// TODO Cleanup the load function. It can currenlty also do insert, which is awkward
+// TODO Watch for background changes...How?
+
+package com.android.contacts.views.edit;
+
+import com.android.contacts.JoinContactActivity;
+import com.android.contacts.R;
+import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.Editor;
+import com.android.contacts.model.EntityDelta;
+import com.android.contacts.model.EntityModifier;
+import com.android.contacts.model.EntitySet;
+import com.android.contacts.model.GoogleSource;
+import com.android.contacts.model.Sources;
+import com.android.contacts.model.ContactsSource.EditType;
+import com.android.contacts.model.Editor.EditorListener;
+import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.ui.ViewIdGenerator;
+import com.android.contacts.ui.widget.BaseContactEditorView;
+import com.android.contacts.ui.widget.PhotoEditorView;
+import com.android.contacts.util.EmptyService;
+import com.android.contacts.util.WeakAsyncTask;
+
+import android.accounts.Account;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.app.patterns.Loader;
+import android.app.patterns.LoaderManagingFragment;
+import android.content.ActivityNotFoundException;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.OperationApplicationException;
+import android.content.ContentProviderOperation.Builder;
+import android.content.DialogInterface.OnDismissListener;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.media.MediaScannerConnection;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.provider.MediaStore;
+import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.LinearLayout;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.io.File;
+import java.lang.ref.WeakReference;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+
+public class ContactEditFragment extends LoaderManagingFragment<ContactEditLoader.Result> {
+
+    private static final String TAG = "ContactEditFragment";
+
+    private static final int LOADER_DATA = 1;
+
+    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_QUERY_SELECTION_ARGS = "queryselectionargs";
+    private static final String KEY_CONTACT_ID_FOR_JOIN = "contactidforjoin";
+
+    public static final int SAVE_MODE_DEFAULT = 0;
+    public static final int SAVE_MODE_SPLIT = 1;
+    public static final int SAVE_MODE_JOIN = 2;
+
+    private long mRawContactIdRequestingPhoto = -1;
+
+    private final EntityDeltaComparator mComparator = new EntityDeltaComparator();
+
+    private static final String BUNDLE_SELECT_ACCOUNT_LIST = "account_list";
+
+    private static final int ICON_SIZE = 96;
+
+    private static final File PHOTO_DIR = new File(
+            Environment.getExternalStorageDirectory() + "/DCIM/Camera");
+
+    private File mCurrentPhotoFile;
+
+    private Context mContext;
+    private String mAction;
+    private Uri mUri;
+    private String mMimeType;
+    private Bundle mIntentExtras;
+    private Callbacks mCallbacks;
+
+    private String mQuerySelection;
+    private String[] mQuerySelectionArgs;
+
+    private long mContactIdForJoin;
+
+    private LinearLayout mContent;
+    private EntitySet mState;
+
+    private ViewIdGenerator mViewIdGenerator;
+
+    public ContactEditFragment() {
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+        mContext = activity;
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
+        final View view = inflater.inflate(R.layout.contact_edit, container, false);
+
+        mContent = (LinearLayout) view.findViewById(R.id.editors);
+
+        view.findViewById(R.id.btn_done).setOnClickListener(new OnClickListener() {
+            public void onClick(View v) {
+                doSaveAction(SAVE_MODE_DEFAULT);
+            }
+        });
+        view.findViewById(R.id.btn_discard).setOnClickListener(new OnClickListener() {
+            public void onClick(View v) {
+                doRevertAction();
+            }
+        });
+
+        return view;
+    }
+
+    // TODO: Think about splitting this. Doing INSERT via load is kinda weird
+    public void load(String action, Uri uri, String mimeType, Bundle intentExtras) {
+        mAction = action;
+        mUri = uri;
+        mMimeType = mimeType;
+        mIntentExtras = intentExtras;
+
+        if (Intent.ACTION_EDIT.equals(mAction)) {
+            // Read initial state from database
+            if (mCallbacks != null) mCallbacks.setTitleTo(R.string.editContact_title_edit);
+            startLoading(LOADER_DATA, null);
+        } else if (Intent.ACTION_INSERT.equals(mAction)) {
+            if (mCallbacks != null) mCallbacks.setTitleTo(R.string.editContact_title_insert);
+
+            doAddAction();
+        } else throw new IllegalArgumentException("Unknown Action String " + mAction +
+                ". Only support " + Intent.ACTION_EDIT + " or " + Intent.ACTION_INSERT);
+    }
+
+    public void setCallbacks(Callbacks callbacks) {
+        mCallbacks = callbacks;
+    }
+
+    @Override
+    public void onCreate(Bundle savedState) {
+        // TODO: Currently savedState is always null (framework issue). Test once this is fixed
+        super.onCreate(savedState);
+
+        if (savedState == null) {
+            // If savedState is non-null, onRestoreInstanceState() will restore the generator.
+            mViewIdGenerator = new ViewIdGenerator();
+        } else {
+            // Read modifications from instance
+            mState = savedState.<EntitySet> getParcelable(KEY_EDIT_STATE);
+            mRawContactIdRequestingPhoto = savedState.getLong(
+                    KEY_RAW_CONTACT_ID_REQUESTING_PHOTO);
+            mViewIdGenerator = savedState.getParcelable(KEY_VIEW_ID_GENERATOR);
+            String fileName = savedState.getString(KEY_CURRENT_PHOTO_FILE);
+            if (fileName != null) {
+                mCurrentPhotoFile = new File(fileName);
+            }
+            mQuerySelection = savedState.getString(KEY_QUERY_SELECTION);
+            mQuerySelectionArgs = savedState.getStringArray(KEY_QUERY_SELECTION_ARGS);
+            mContactIdForJoin = savedState.getLong(KEY_CONTACT_ID_FOR_JOIN);
+        }
+    }
+
+    @Override
+    protected Loader<ContactEditLoader.Result> onCreateLoader(int id, Bundle args) {
+        return new ContactEditLoader(mContext, mUri, mMimeType, mIntentExtras);
+    }
+
+    @Override
+    protected void onInitializeLoaders() {
+    }
+
+    @Override
+    protected void onLoadFinished(Loader<ContactEditLoader.Result> loader,
+            ContactEditLoader.Result data) {
+        if (data == ContactEditLoader.Result.NOT_FOUND) {
+            // Item has been deleted
+            Log.i(TAG, "No contact found. Closing fragment");
+            if (mCallbacks != null) mCallbacks.closeBecauseContactNotFound();
+            return;
+        }
+        setData(data);
+    }
+
+    public void setData(ContactEditLoader.Result data) {
+        mState = data.getEntitySet();
+        bindEditors();
+    }
+
+    public void selectAccountAndCreateContact(ArrayList<Account> accounts, boolean isNewContact) {
+        // No Accounts available.  Create a phone-local contact.
+        if (accounts.isEmpty()) {
+            createContact(null, isNewContact);
+            return;  // Don't show a dialog.
+        }
+
+        // In the common case of a single account being writable, auto-select
+        // it without showing a dialog.
+        if (accounts.size() == 1) {
+            createContact(accounts.get(0), isNewContact);
+            return;  // Don't show a dialog.
+        }
+
+        Bundle bundle = new Bundle();
+        bundle.putParcelableArrayList(BUNDLE_SELECT_ACCOUNT_LIST, accounts);
+        getActivity().showDialog(R.id.edit_dialog_select_account, bundle);
+    }
+
+    /**
+     * @param account may be null to signal a device-local contact should
+     *     be created.
+     * @param prefillFromIntent If this is set, the intent extras will be used to prefill the fields
+     */
+    private void createContact(Account account, boolean prefillFromIntent) {
+        final Sources sources = Sources.getInstance(mContext);
+        final ContentValues values = new ContentValues();
+        if (account != null) {
+            values.put(RawContacts.ACCOUNT_NAME, account.name);
+            values.put(RawContacts.ACCOUNT_TYPE, account.type);
+        } else {
+            values.putNull(RawContacts.ACCOUNT_NAME);
+            values.putNull(RawContacts.ACCOUNT_TYPE);
+        }
+
+        // Parse any values from incoming intent
+        EntityDelta insert = new EntityDelta(ValuesDelta.fromAfter(values));
+        final ContactsSource source = sources.getInflatedSource(
+                account != null ? account.type : null,
+                ContactsSource.LEVEL_CONSTRAINTS);
+        EntityModifier.parseExtras(mContext, source, insert,
+                prefillFromIntent ? mIntentExtras : null);
+
+        // Ensure we have some default fields
+        EntityModifier.ensureKindExists(insert, source, Phone.CONTENT_ITEM_TYPE);
+        EntityModifier.ensureKindExists(insert, source, Email.CONTENT_ITEM_TYPE);
+
+        if (mState == null) {
+            // Create state if none exists yet
+            mState = EntitySet.fromSingle(insert);
+        } else {
+            // Add contact onto end of existing state
+            mState.add(insert);
+        }
+
+        bindEditors();
+    }
+
+    private void bindEditors() {
+        // Sort the editors
+        Collections.sort(mState, mComparator);
+
+        // Remove any existing editors and rebuild any visible
+        mContent.removeAllViews();
+
+        final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+        final Sources sources = Sources.getInstance(mContext);
+        int size = mState.size();
+        for (int i = 0; i < size; i++) {
+            // TODO ensure proper ordering of entities in the list
+            final EntityDelta entity = mState.get(i);
+            final ValuesDelta values = entity.getValues();
+            if (!values.isVisible()) continue;
+
+            final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
+            final ContactsSource source = sources.getInflatedSource(accountType,
+                    ContactsSource.LEVEL_CONSTRAINTS);
+            final long rawContactId = values.getAsLong(RawContacts._ID);
+
+            final BaseContactEditorView editor;
+            if (!source.readOnly) {
+                editor = (BaseContactEditorView) inflater.inflate(R.layout.item_contact_editor,
+                        mContent, false);
+            } else {
+                editor = (BaseContactEditorView) inflater.inflate(
+                        R.layout.item_read_only_contact_editor, mContent, false);
+            }
+            final PhotoEditorView photoEditor = editor.getPhotoEditor();
+            photoEditor.setEditorListener(new PhotoListener(rawContactId, source.readOnly,
+                    photoEditor));
+
+            mContent.addView(editor);
+            editor.setState(entity, source, mViewIdGenerator);
+        }
+
+        // Show editor now that we've loaded state
+        mContent.setVisibility(View.VISIBLE);
+    }
+
+    public boolean onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
+        inflater.inflate(R.menu.edit, menu);
+
+        return true;
+    }
+
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        menu.findItem(R.id.menu_split).setVisible(mState != null && mState.size() > 1);
+        return true;
+    }
+
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.menu_done:
+                return doSaveAction(SAVE_MODE_DEFAULT);
+            case R.id.menu_discard:
+                return doRevertAction();
+            case R.id.menu_add:
+                return doAddAction();
+            case R.id.menu_delete:
+                return doDeleteAction();
+            case R.id.menu_split:
+                return doSplitContactAction();
+            case R.id.menu_join:
+                return doJoinContactAction();
+        }
+        return false;
+    }
+
+    private boolean doAddAction() {
+        // Load Accounts async so that we can present them
+        AsyncTask<Void, Void, ArrayList<Account>> loadAccountsTask =
+                new AsyncTask<Void, Void, ArrayList<Account>>() {
+                    @Override
+                    protected ArrayList<Account> doInBackground(Void... params) {
+                        return Sources.getInstance(mContext).getAccounts(true);
+                    }
+                    @Override
+                    protected void onPostExecute(ArrayList<Account> result) {
+                        selectAccountAndCreateContact(result, true);
+                    }
+        };
+        loadAccountsTask.execute();
+
+        return true;
+    }
+
+    /**
+     * Delete the entire contact currently being edited, which usually asks for
+     * user confirmation before continuing.
+     */
+    private boolean doDeleteAction() {
+        if (!hasValidState())
+            return false;
+        int readOnlySourcesCnt = 0;
+        int writableSourcesCnt = 0;
+        // TODO: This shouldn't be called from the UI thread
+        final Sources sources = Sources.getInstance(mContext);
+        for (EntityDelta delta : mState) {
+            final String accountType = delta.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+            final ContactsSource contactsSource = sources.getInflatedSource(accountType,
+                    ContactsSource.LEVEL_CONSTRAINTS);
+            if (contactsSource != null && contactsSource.readOnly) {
+                readOnlySourcesCnt += 1;
+            } else {
+                writableSourcesCnt += 1;
+            }
+        }
+
+        if (readOnlySourcesCnt > 0 && writableSourcesCnt > 0) {
+            getActivity().showDialog(R.id.edit_dialog_confirm_readonly_delete);
+        } else if (readOnlySourcesCnt > 0 && writableSourcesCnt == 0) {
+            getActivity().showDialog(R.id.edit_dialog_confirm_readonly_hide);
+        } else if (readOnlySourcesCnt == 0 && writableSourcesCnt > 1) {
+            getActivity().showDialog(R.id.edit_dialog_confirm_multiple_delete);
+        } else {
+            getActivity().showDialog(R.id.edit_dialog_confirm_delete);
+        }
+        return true;
+    }
+
+    /**
+     * Pick a specific photo to be added under the currently selected tab.
+     */
+    /* package */ boolean doPickPhotoAction(long rawContactId) {
+        if (!hasValidState()) return false;
+
+        mRawContactIdRequestingPhoto = rawContactId;
+
+        getActivity().showDialog(R.id.edit_dialog_pick_photo);
+        return false;
+    }
+
+    public Dialog onCreateDialog(int id, Bundle bundle) {
+        switch (id) {
+            case R.id.edit_dialog_confirm_delete:
+                return new AlertDialog.Builder(mContext)
+                        .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())
+                        .setCancelable(false)
+                        .create();
+            case R.id.edit_dialog_confirm_readonly_delete:
+                return new AlertDialog.Builder(mContext)
+                        .setTitle(R.string.deleteConfirmation_title)
+                        .setIcon(android.R.drawable.ic_dialog_alert)
+                        .setMessage(R.string.readOnlyContactDeleteConfirmation)
+                        .setNegativeButton(android.R.string.cancel, null)
+                        .setPositiveButton(android.R.string.ok, new DeleteClickListener())
+                        .setCancelable(false)
+                        .create();
+            case R.id.edit_dialog_confirm_multiple_delete:
+                return new AlertDialog.Builder(mContext)
+                        .setTitle(R.string.deleteConfirmation_title)
+                        .setIcon(android.R.drawable.ic_dialog_alert)
+                        .setMessage(R.string.multipleContactDeleteConfirmation)
+                        .setNegativeButton(android.R.string.cancel, null)
+                        .setPositiveButton(android.R.string.ok, new DeleteClickListener())
+                        .setCancelable(false)
+                        .create();
+            case R.id.edit_dialog_confirm_readonly_hide:
+                return new AlertDialog.Builder(mContext)
+                        .setTitle(R.string.deleteConfirmation_title)
+                        .setIcon(android.R.drawable.ic_dialog_alert)
+                        .setMessage(R.string.readOnlyContactWarning)
+                        .setPositiveButton(android.R.string.ok, new DeleteClickListener())
+                        .setCancelable(false)
+                        .create();
+            case R.id.edit_dialog_pick_photo:
+                return createPickPhotoDialog();
+            case R.id.edit_dialog_split:
+                return createSplitDialog();
+            case R.id.edit_dialog_select_account:
+                return createSelectAccountDialog(bundle);
+            default:
+                return null;
+        }
+    }
+
+    private Dialog createSelectAccountDialog(Bundle bundle) {
+        final ArrayList<Account> accounts = bundle.getParcelableArrayList(
+                BUNDLE_SELECT_ACCOUNT_LIST);
+        // Wrap our context to inflate list items using correct theme
+        final Context dialogContext = new ContextThemeWrapper(mContext, android.R.style.Theme_Light);
+        final LayoutInflater dialogInflater =
+            (LayoutInflater)dialogContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+        final Sources sources = Sources.getInstance(mContext);
+
+        final ArrayAdapter<Account> accountAdapter = new ArrayAdapter<Account>(mContext,
+                android.R.layout.simple_list_item_2, accounts) {
+            @Override
+            public View getView(int position, View convertView, ViewGroup parent) {
+                if (convertView == null) {
+                    convertView = dialogInflater.inflate(android.R.layout.simple_list_item_2,
+                            parent, false);
+                }
+
+                // TODO: show icon along with title
+                final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
+                final TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
+
+                final Account account = this.getItem(position);
+                final ContactsSource source = sources.getInflatedSource(account.type,
+                        ContactsSource.LEVEL_SUMMARY);
+
+                text1.setText(account.name);
+                text2.setText(source.getDisplayLabel(mContext));
+
+                return convertView;
+            }
+        };
+
+        final DialogInterface.OnClickListener clickListener =
+                new DialogInterface.OnClickListener() {
+            public void onClick(DialogInterface dialog, int which) {
+                dialog.dismiss();
+
+                // Create new contact based on selected source
+                final Account account = accountAdapter.getItem(which);
+                createContact(account, false);
+            }
+        };
+
+        final DialogInterface.OnCancelListener cancelListener =
+                new DialogInterface.OnCancelListener() {
+            public void onCancel(DialogInterface dialog) {
+                // If nothing remains, close activity
+                if (!hasValidState()) {
+                    mCallbacks.closeBecauseAccountSelectorAborted();
+                }
+            }
+        };
+
+        final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
+        builder.setTitle(R.string.dialog_new_contact_account);
+        builder.setSingleChoiceItems(accountAdapter, 0, clickListener);
+        builder.setOnCancelListener(cancelListener);
+        final Dialog result = builder.create();
+        result.setOnDismissListener(new OnDismissListener() {
+            public void onDismiss(DialogInterface dialog) {
+                // TODO: Check if we even need this...seems useless
+                //removeDialog(DIALOG_SELECT_ACCOUNT);
+            }
+        });
+        return result;
+    }
+
+    private boolean doSplitContactAction() {
+        if (!hasValidState()) return false;
+
+        getActivity().showDialog(R.id.edit_dialog_split);
+        return true;
+    }
+
+    private Dialog createSplitDialog() {
+        final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
+        builder.setTitle(R.string.splitConfirmation_title);
+        builder.setIcon(android.R.drawable.ic_dialog_alert);
+        builder.setMessage(R.string.splitConfirmation);
+        builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+            public void onClick(DialogInterface dialog, int which) {
+                // Split the contacts
+                mState.splitRawContacts();
+                doSaveAction(SAVE_MODE_SPLIT);
+            }
+        });
+        builder.setNegativeButton(android.R.string.cancel, null);
+        builder.setCancelable(false);
+        return builder.create();
+    }
+
+    private boolean doJoinContactAction() {
+        return doSaveAction(SAVE_MODE_JOIN);
+    }
+
+    /**
+     * Creates a dialog offering two options: take a photo or pick a photo from the gallery.
+     */
+    private Dialog createPickPhotoDialog() {
+        // Wrap our context to inflate list items using correct theme
+        final Context dialogContext = new ContextThemeWrapper(mContext,
+                android.R.style.Theme_Light);
+
+        String[] choices = new String[2];
+        choices[0] = mContext.getString(R.string.take_photo);
+        choices[1] = mContext.getString(R.string.pick_photo);
+        final ListAdapter adapter = new ArrayAdapter<String>(dialogContext,
+                android.R.layout.simple_list_item_1, choices);
+
+        final AlertDialog.Builder builder = new AlertDialog.Builder(dialogContext);
+        builder.setTitle(R.string.attachToContact);
+        builder.setSingleChoiceItems(adapter, -1, new DialogInterface.OnClickListener() {
+            public void onClick(DialogInterface dialog, int which) {
+                dialog.dismiss();
+                switch(which) {
+                    case 0:
+                        doTakePhoto();
+                        break;
+                    case 1:
+                        doPickPhotoFromGallery();
+                        break;
+                }
+            }
+        });
+        return builder.create();
+    }
+
+
+    /**
+     * Launches Gallery to pick a photo.
+     */
+    protected void doPickPhotoFromGallery() {
+        try {
+            // Launch picker to choose photo for selected contact
+            final Intent intent = getPhotoPickIntent();
+            getActivity().startActivityForResult(intent,
+                    R.id.edit_request_code_photo_picked_with_data);
+        } catch (ActivityNotFoundException e) {
+            Toast.makeText(mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
+        }
+    }
+
+    /**
+     * Constructs an intent for picking a photo from Gallery, cropping it and returning the bitmap.
+     */
+    public static Intent getPhotoPickIntent() {
+        Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
+        intent.setType("image/*");
+        intent.putExtra("crop", "true");
+        intent.putExtra("aspectX", 1);
+        intent.putExtra("aspectY", 1);
+        intent.putExtra("outputX", ICON_SIZE);
+        intent.putExtra("outputY", ICON_SIZE);
+        intent.putExtra("return-data", true);
+        return intent;
+    }
+
+    /**
+     * Check if our internal {@link #mState} is valid, usually checked before
+     * performing user actions.
+     */
+    private boolean hasValidState() {
+        return mState != null && mState.size() > 0;
+    }
+
+    /**
+     * Create a file name for the icon photo using current time.
+     */
+    private String getPhotoFileName() {
+        Date date = new Date(System.currentTimeMillis());
+        SimpleDateFormat dateFormat = new SimpleDateFormat("'IMG'_yyyyMMdd_HHmmss");
+        return dateFormat.format(date) + ".jpg";
+    }
+
+    /**
+     * Launches Camera to take a picture and store it in a file.
+     */
+    protected void doTakePhoto() {
+        try {
+            // Launch camera to take photo for selected contact
+            PHOTO_DIR.mkdirs();
+            mCurrentPhotoFile = new File(PHOTO_DIR, getPhotoFileName());
+            final Intent intent = getTakePickIntent(mCurrentPhotoFile);
+
+            getActivity().startActivityForResult(intent, R.id.edit_request_code_camera_with_data);
+        } catch (ActivityNotFoundException e) {
+            Toast.makeText(mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
+        }
+    }
+
+    /**
+     * Constructs an intent for capturing a photo and storing it in a temporary file.
+     */
+    public static Intent getTakePickIntent(File f) {
+        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE, null);
+        intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(f));
+        return intent;
+    }
+
+    /**
+     * Sends a newly acquired photo to Gallery for cropping
+     */
+    protected void doCropPhoto(File f) {
+        try {
+            // Add the image to the media store
+            MediaScannerConnection.scanFile(
+                    mContext,
+                    new String[] { f.getAbsolutePath() },
+                    new String[] { null },
+                    null);
+
+            // Launch gallery to crop the photo
+            final Intent intent = getCropImageIntent(Uri.fromFile(f));
+            getActivity().startActivityForResult(intent,
+                    R.id.edit_request_code_photo_picked_with_data);
+        } catch (Exception e) {
+            Log.e(TAG, "Cannot crop image", e);
+            Toast.makeText(mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
+        }
+    }
+
+    /**
+     * Constructs an intent for image cropping.
+     */
+    public static Intent getCropImageIntent(Uri photoUri) {
+        Intent intent = new Intent("com.android.camera.action.CROP");
+        intent.setDataAndType(photoUri, "image/*");
+        intent.putExtra("crop", "true");
+        intent.putExtra("aspectX", 1);
+        intent.putExtra("aspectY", 1);
+        intent.putExtra("outputX", ICON_SIZE);
+        intent.putExtra("outputY", ICON_SIZE);
+        intent.putExtra("return-data", true);
+        return intent;
+    }
+
+    /**
+     * Saves or creates the contact based on the mode, and if successful
+     * finishes the activity.
+     */
+    private boolean doSaveAction(int saveMode) {
+        if (!hasValidState()) {
+            return false;
+        }
+
+        // TODO: Status still needed?
+        //mStatus = STATUS_SAVING;
+        final PersistTask task = new PersistTask(this, saveMode);
+        task.execute(mState);
+
+        return true;
+    }
+
+    private boolean doRevertAction() {
+        if (mCallbacks != null) mCallbacks.closeAfterRevert();
+
+        return true;
+    }
+
+    private void onSaveCompleted(boolean success, int saveMode, Uri contactLookupUri) {
+        switch (saveMode) {
+            case SAVE_MODE_DEFAULT:
+                final Intent resultIntent;
+                final int resultCode;
+                if (success && contactLookupUri != null) {
+                    final String requestAuthority = mUri == null ? null : mUri.getAuthority();
+
+                    final String legacyAuthority = "contacts";
+
+                    resultIntent = new Intent();
+                    if (legacyAuthority.equals(requestAuthority)) {
+                        // Build legacy Uri when requested by caller
+                        final long contactId = ContentUris.parseId(Contacts.lookupContact(
+                                mContext.getContentResolver(), contactLookupUri));
+                        final Uri legacyContentUri = Uri.parse("content://contacts/people");
+                        final Uri legacyUri = ContentUris.withAppendedId(
+                                legacyContentUri, contactId);
+                        resultIntent.setData(legacyUri);
+                    } else {
+                        // Otherwise pass back a lookup-style Uri
+                        resultIntent.setData(contactLookupUri);
+                    }
+
+                    resultCode = Activity.RESULT_OK;
+                } else {
+                    resultCode = Activity.RESULT_CANCELED;
+                    resultIntent = null;
+                }
+                if (mCallbacks != null) mCallbacks.closeAfterSaving(resultCode, resultIntent);
+                break;
+            case SAVE_MODE_SPLIT:
+                if (mCallbacks != null) mCallbacks.closeAfterSplit();
+                break;
+
+            case SAVE_MODE_JOIN:
+                //mStatus = STATUS_EDITING;
+                if (success) {
+                    showJoinAggregateActivity(contactLookupUri);
+                }
+                break;
+        }
+    }
+
+    /**
+     * Shows a list of aggregates that can be joined into the currently viewed aggregate.
+     *
+     * @param contactLookupUri the fresh URI for the currently edited contact (after saving it)
+     */
+    private void showJoinAggregateActivity(Uri contactLookupUri) {
+        if (contactLookupUri == null) {
+            return;
+        }
+
+        mContactIdForJoin = ContentUris.parseId(contactLookupUri);
+        final Intent intent = new Intent(JoinContactActivity.JOIN_CONTACT);
+        intent.putExtra(JoinContactActivity.EXTRA_TARGET_CONTACT_ID, mContactIdForJoin);
+        getActivity().startActivityForResult(intent, R.id.edit_request_code_join);
+    }
+
+    private interface JoinContactQuery {
+        String[] PROJECTION = {
+                RawContacts._ID,
+                RawContacts.CONTACT_ID,
+                RawContacts.NAME_VERIFIED,
+        };
+
+        String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
+
+        int _ID = 0;
+        int CONTACT_ID = 1;
+        int NAME_VERIFIED = 2;
+    }
+
+    /**
+     * Performs aggregation with the contact selected by the user from suggestions or A-Z list.
+     */
+    private void joinAggregate(final long contactId) {
+        final ContentResolver resolver = mContext.getContentResolver();
+
+        // Load raw contact IDs for all raw contacts involved - currently edited and selected
+        // in the join UIs
+        Cursor c = resolver.query(RawContacts.CONTENT_URI,
+                JoinContactQuery.PROJECTION,
+                JoinContactQuery.SELECTION,
+                new String[]{String.valueOf(contactId), String.valueOf(mContactIdForJoin)}, null);
+
+        long rawContactIds[];
+        long verifiedNameRawContactId = -1;
+        try {
+            rawContactIds = new long[c.getCount()];
+            for (int i = 0; i < rawContactIds.length; i++) {
+                c.moveToNext();
+                long rawContactId = c.getLong(JoinContactQuery._ID);
+                rawContactIds[i] = rawContactId;
+                if (c.getLong(JoinContactQuery.CONTACT_ID) == mContactIdForJoin) {
+                    if (verifiedNameRawContactId == -1
+                            || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0) {
+                        verifiedNameRawContactId = rawContactId;
+                    }
+                }
+            }
+        } finally {
+            c.close();
+        }
+
+        // For each pair of raw contacts, insert an aggregation exception
+        ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
+        for (int i = 0; i < rawContactIds.length; i++) {
+            for (int j = 0; j < rawContactIds.length; j++) {
+                if (i != j) {
+                    buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
+                }
+            }
+        }
+
+        // Mark the original contact as "name verified" to make sure that the contact
+        // display name does not change as a result of the join
+        Builder builder = ContentProviderOperation.newUpdate(
+                    ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
+        builder.withValue(RawContacts.NAME_VERIFIED, 1);
+        operations.add(builder.build());
+
+        // Apply all aggregation exceptions as one batch
+        try {
+            resolver.applyBatch(ContactsContract.AUTHORITY, operations);
+
+            // We can use any of the constituent raw contacts to refresh the UI - why not the first
+            final Intent intent = new Intent();
+            intent.setData(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
+
+            // Reload the new state from database
+            // TODO: Reload necessary or do we have a listener?
+            //new QueryEntitiesTask(this).execute(intent);
+
+            Toast.makeText(mContext, R.string.contactsJoinedMessage, Toast.LENGTH_LONG).show();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to apply aggregation exception batch", e);
+            Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
+        } catch (OperationApplicationException e) {
+            Log.e(TAG, "Failed to apply aggregation exception batch", e);
+            Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
+        }
+    }
+
+    /**
+     * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
+     */
+    private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
+            long rawContactId1, long rawContactId2) {
+        Builder builder =
+                ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
+        builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
+        builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
+        builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
+        operations.add(builder.build());
+    }
+
+    public static interface Callbacks {
+        /**
+         * Contact was not found, so somehow close this fragment.
+         */
+        void closeBecauseContactNotFound();
+
+        /**
+         * Contact was split, so we can close now
+         */
+        void closeAfterSplit();
+
+        /**
+         * User was presented with an account selection and couldn't decide.
+         */
+        void closeBecauseAccountSelectorAborted();
+
+        /**
+         * User has tapped Revert, close the fragment now.
+         */
+        void closeAfterRevert();
+
+        /**
+         * User has removed the contact, close the fragment now.
+         */
+        void closeAfterDelete();
+
+        /**
+         * Set the Title (e.g. of the Activity)
+         */
+        void setTitleTo(int resourceId);
+
+        /**
+         * Contact was
+         * @param resultCode
+         * @param resultIntent
+         */
+        void closeAfterSaving(int resultCode, Intent resultIntent);
+    }
+
+    private class EntityDeltaComparator implements Comparator<EntityDelta> {
+        /**
+         * Compare EntityDeltas for sorting the stack of editors.
+         */
+        public int compare(EntityDelta one, EntityDelta two) {
+            // Check direct equality
+            if (one.equals(two)) {
+                return 0;
+            }
+
+            final Sources sources = Sources.getInstance(mContext);
+            String accountType = one.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+            final ContactsSource oneSource = sources.getInflatedSource(accountType,
+                    ContactsSource.LEVEL_SUMMARY);
+            accountType = two.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+            final ContactsSource twoSource = sources.getInflatedSource(accountType,
+                    ContactsSource.LEVEL_SUMMARY);
+
+            // Check read-only
+            if (oneSource.readOnly && !twoSource.readOnly) {
+                return 1;
+            } else if (twoSource.readOnly && !oneSource.readOnly) {
+                return -1;
+            }
+
+            // Check account type
+            boolean skipAccountTypeCheck = false;
+            boolean oneIsGoogle = oneSource instanceof GoogleSource;
+            boolean twoIsGoogle = twoSource instanceof GoogleSource;
+            if (oneIsGoogle && !twoIsGoogle) {
+                return -1;
+            } else if (twoIsGoogle && !oneIsGoogle) {
+                return 1;
+            } else if (oneIsGoogle && twoIsGoogle){
+                skipAccountTypeCheck = true;
+            }
+
+            int value;
+            if (!skipAccountTypeCheck) {
+                value = oneSource.accountType.compareTo(twoSource.accountType);
+                if (value != 0) {
+                    return value;
+                }
+            }
+
+            // Check account name
+            ValuesDelta oneValues = one.getValues();
+            String oneAccount = oneValues.getAsString(RawContacts.ACCOUNT_NAME);
+            if (oneAccount == null) oneAccount = "";
+            ValuesDelta twoValues = two.getValues();
+            String twoAccount = twoValues.getAsString(RawContacts.ACCOUNT_NAME);
+            if (twoAccount == null) twoAccount = "";
+            value = oneAccount.compareTo(twoAccount);
+            if (value != 0) {
+                return value;
+            }
+
+            // Both are in the same account, fall back to contact ID
+            Long oneId = oneValues.getAsLong(RawContacts._ID);
+            Long twoId = twoValues.getAsLong(RawContacts._ID);
+            if (oneId == null) {
+                return -1;
+            } else if (twoId == null) {
+                return 1;
+            }
+
+            return (int)(oneId - twoId);
+        }
+    }
+
+    /**
+     * Class that listens to requests coming from photo editors
+     */
+    private class PhotoListener implements EditorListener, DialogInterface.OnClickListener {
+        private long mRawContactId;
+        private boolean mReadOnly;
+        private PhotoEditorView mEditor;
+
+        public PhotoListener(long rawContactId, boolean readOnly, PhotoEditorView editor) {
+            mRawContactId = rawContactId;
+            mReadOnly = readOnly;
+            mEditor = editor;
+        }
+
+        public void onDeleted(Editor editor) {
+            // Do nothing
+        }
+
+        public void onRequest(int request) {
+            if (!hasValidState()) return;
+
+            if (request == EditorListener.REQUEST_PICK_PHOTO) {
+                if (mEditor.hasSetPhoto()) {
+                    // There is an existing photo, offer to remove, replace, or promoto to primary
+                    createPhotoDialog().show();
+                } else if (!mReadOnly) {
+                    // No photo set and not read-only, try to set the photo
+                    doPickPhotoAction(mRawContactId);
+                }
+            }
+        }
+
+        /**
+         * Prepare dialog for picking a new {@link EditType} or entering a
+         * custom label. This dialog is limited to the valid types as determined
+         * by {@link EntityModifier}.
+         */
+        public Dialog createPhotoDialog() {
+            // Wrap our context to inflate list items using correct theme
+            final Context dialogContext = new ContextThemeWrapper(mContext,
+                    android.R.style.Theme_Light);
+
+            String[] choices;
+            if (mReadOnly) {
+                choices = new String[1];
+                choices[0] = mContext.getString(R.string.use_photo_as_primary);
+            } else {
+                choices = new String[3];
+                choices[0] = mContext.getString(R.string.use_photo_as_primary);
+                choices[1] = mContext.getString(R.string.removePicture);
+                choices[2] = mContext.getString(R.string.changePicture);
+            }
+            final ListAdapter adapter = new ArrayAdapter<String>(dialogContext,
+                    android.R.layout.simple_list_item_1, choices);
+
+            final AlertDialog.Builder builder = new AlertDialog.Builder(dialogContext);
+            builder.setTitle(R.string.attachToContact);
+            builder.setSingleChoiceItems(adapter, -1, this);
+            return builder.create();
+        }
+
+        /**
+         * Called when something in the dialog is clicked
+         */
+        public void onClick(DialogInterface dialog, int which) {
+            dialog.dismiss();
+
+            switch (which) {
+                case 0:
+                    // Set the photo as super primary
+                    mEditor.setSuperPrimary(true);
+
+                    // And set all other photos as not super primary
+                    int count = mContent.getChildCount();
+                    for (int i = 0; i < count; i++) {
+                        View childView = mContent.getChildAt(i);
+                        if (childView instanceof BaseContactEditorView) {
+                            BaseContactEditorView editor = (BaseContactEditorView) childView;
+                            PhotoEditorView photoEditor = editor.getPhotoEditor();
+                            if (!photoEditor.equals(mEditor)) {
+                                photoEditor.setSuperPrimary(false);
+                            }
+                        }
+                    }
+                    break;
+
+                case 1:
+                    // Remove the photo
+                    mEditor.setPhotoBitmap(null);
+                    break;
+
+                case 2:
+                    // Pick a new photo for the contact
+                    doPickPhotoAction(mRawContactId);
+                    break;
+            }
+        }
+    }
+
+
+    private class DeleteClickListener implements DialogInterface.OnClickListener {
+        public void onClick(DialogInterface dialog, int which) {
+            // TODO: Don't do this from the UI thread
+            final Sources sources = Sources.getInstance(mContext);
+            // Mark all raw contacts for deletion
+            for (EntityDelta delta : mState) {
+                delta.markDeleted();
+            }
+            // Save the deletes
+            doSaveAction(SAVE_MODE_DEFAULT);
+            mCallbacks.closeAfterDelete();
+        }
+    }
+
+    // TODO: There has to be a nicer way than this WeakAsyncTask...? Maybe call a service?
+    /**
+     * Background task for persisting edited contact data, using the changes
+     * defined by a set of {@link EntityDelta}. This task starts
+     * {@link EmptyService} to make sure the background thread can finish
+     * persisting in cases where the system wants to reclaim our process.
+     */
+    public static class PersistTask extends
+            WeakAsyncTask<EntitySet, Void, Integer, ContactEditFragment> {
+        private static final int PERSIST_TRIES = 3;
+
+        private static final int RESULT_UNCHANGED = 0;
+        private static final int RESULT_SUCCESS = 1;
+        private static final int RESULT_FAILURE = 2;
+
+        private WeakReference<ProgressDialog> mProgress;
+        private final Context mContext;
+
+        private int mSaveMode;
+        private Uri mContactLookupUri = null;
+
+        public PersistTask(ContactEditFragment target, int saveMode) {
+            super(target);
+            mSaveMode = saveMode;
+            mContext = target.mContext;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected void onPreExecute(ContactEditFragment target) {
+            mProgress = new WeakReference<ProgressDialog>(ProgressDialog.show(mContext, null,
+                    mContext.getText(R.string.savingContact)));
+
+            // Before starting this task, start an empty service to protect our
+            // process from being reclaimed by the system.
+            mContext.startService(new Intent(mContext, EmptyService.class));
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected Integer doInBackground(ContactEditFragment target, EntitySet... params) {
+            final ContentResolver resolver = mContext.getContentResolver();
+
+            EntitySet state = params[0];
+
+            // Trim any empty fields, and RawContacts, before persisting
+            final Sources sources = Sources.getInstance(mContext);
+            EntityModifier.trimEmpty(state, sources);
+
+            // Attempt to persist changes
+            int tries = 0;
+            Integer result = RESULT_FAILURE;
+            while (tries++ < PERSIST_TRIES) {
+                try {
+                    // Build operations and try applying
+                    final ArrayList<ContentProviderOperation> diff = state.buildDiff();
+                    ContentProviderResult[] results = null;
+                    if (!diff.isEmpty()) {
+                         results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
+                    }
+
+                    final long rawContactId = getRawContactId(state, diff, results);
+                    if (rawContactId != -1) {
+                        final Uri rawContactUri = ContentUris.withAppendedId(
+                                RawContacts.CONTENT_URI, rawContactId);
+
+                        // convert the raw contact URI to a contact URI
+                        mContactLookupUri = RawContacts.getContactLookupUri(resolver,
+                                rawContactUri);
+                    }
+                    result = (diff.size() > 0) ? RESULT_SUCCESS : RESULT_UNCHANGED;
+                    break;
+
+                } catch (RemoteException e) {
+                    // Something went wrong, bail without success
+                    Log.e(TAG, "Problem persisting user edits", e);
+                    break;
+
+                } catch (OperationApplicationException e) {
+                    // Version consistency failed, re-parent change and try again
+                    Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
+                    final EntitySet newState = EntitySet.fromQuery(resolver,
+                            target.mQuerySelection, target.mQuerySelectionArgs, null);
+                    state = EntitySet.mergeAfter(newState, state);
+                }
+            }
+
+            return result;
+        }
+
+        private long getRawContactId(EntitySet state,
+                final ArrayList<ContentProviderOperation> diff,
+                final ContentProviderResult[] results) {
+            long rawContactId = state.findRawContactId();
+            if (rawContactId != -1) {
+                return rawContactId;
+            }
+
+            // we gotta do some searching for the id
+            final int diffSize = diff.size();
+            for (int i = 0; i < diffSize; i++) {
+                ContentProviderOperation operation = diff.get(i);
+                if (operation.getType() == ContentProviderOperation.TYPE_INSERT
+                        && operation.getUri().getEncodedPath().contains(
+                                RawContacts.CONTENT_URI.getEncodedPath())) {
+                    return ContentUris.parseId(results[i].uri);
+                }
+            }
+            return -1;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected void onPostExecute(ContactEditFragment target, Integer result) {
+            final ProgressDialog progress = mProgress.get();
+
+            if (result == RESULT_SUCCESS && mSaveMode != SAVE_MODE_JOIN) {
+                Toast.makeText(mContext, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
+            } else if (result == RESULT_FAILURE) {
+                Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
+            }
+
+            if (progress != null) progress.dismiss();
+
+            // Stop the service that was protecting us
+            mContext.stopService(new Intent(mContext, EmptyService.class));
+
+            target.onSaveCompleted(result != RESULT_FAILURE, mSaveMode, mContactLookupUri);
+        }
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        if (hasValidState()) {
+            // Store entities with modifications
+            outState.putParcelable(KEY_EDIT_STATE, mState);
+        }
+
+        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.putStringArray(KEY_QUERY_SELECTION_ARGS, mQuerySelectionArgs);
+        outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin);
+        super.onSaveInstanceState(outState);
+    }
+
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        // Ignore failed requests
+        if (resultCode != Activity.RESULT_OK) return;
+        switch (requestCode) {
+            case R.id.edit_request_code_photo_picked_with_data: {
+                BaseContactEditorView requestingEditor = null;
+                for (int i = 0; i < mContent.getChildCount(); i++) {
+                    View childView = mContent.getChildAt(i);
+                    if (childView instanceof BaseContactEditorView) {
+                        BaseContactEditorView editor = (BaseContactEditorView) childView;
+                        if (editor.getRawContactId() == mRawContactIdRequestingPhoto) {
+                            requestingEditor = editor;
+                            break;
+                        }
+                    }
+                }
+
+                if (requestingEditor != null) {
+                    final Bitmap photo = data.getParcelableExtra("data");
+                    requestingEditor.setPhotoBitmap(photo);
+                    mRawContactIdRequestingPhoto = -1;
+                } else {
+                    // The contact that requested the photo is no longer present.
+                    // TODO: Show error message
+                }
+
+                break;
+            }
+
+            case R.id.edit_request_code_camera_with_data: {
+                doCropPhoto(mCurrentPhotoFile);
+                break;
+            }
+            case R.id.edit_request_code_join: {
+                if (data != null) {
+                    final long contactId = ContentUris.parseId(data.getData());
+                    joinAggregate(contactId);
+                }
+            }
+        }
+    }
+}
diff --git a/src/com/android/contacts/views/edit/ContactEditLoader.java b/src/com/android/contacts/views/edit/ContactEditLoader.java
new file mode 100644
index 0000000..3506c59
--- /dev/null
+++ b/src/com/android/contacts/views/edit/ContactEditLoader.java
@@ -0,0 +1,177 @@
+package com.android.contacts.views.edit;
+
+import com.android.contacts.ContactsUtils;
+import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.EntityDelta;
+import com.android.contacts.model.EntityModifier;
+import com.android.contacts.model.EntitySet;
+import com.android.contacts.model.Sources;
+
+import android.app.patterns.Loader;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+
+public class ContactEditLoader extends Loader<ContactEditLoader.Result> {
+    private static final String TAG = "ContactEditLoader";
+
+    private final Uri mLookupUri;
+    private final String mMimeType;
+    private Result mContact;
+    private boolean mDestroyed;
+    private ForceLoadContentObserver mObserver;
+    private final Bundle mIntentExtras;
+
+    public ContactEditLoader(Context context, Uri lookupUri, String mimeType,
+            Bundle intentExtras) {
+        super(context);
+        mLookupUri = lookupUri;
+        mMimeType = mimeType;
+        mIntentExtras = intentExtras;
+    }
+
+    /**
+     * The result of a load operation. Contains all data necessary to display the contact for
+     * editing.
+     */
+    public static class Result {
+        /**
+         * Singleton instance that represents "No Contact Found"
+         */
+        public static final Result NOT_FOUND = new Result(null);
+
+        private final EntitySet mEntitySet;
+
+        private Result(EntitySet entitySet) {
+            mEntitySet = entitySet;
+        }
+
+        public EntitySet getEntitySet() {
+            return mEntitySet;
+        }
+    }
+
+    private final class LoadContactTask extends AsyncTask<Void, Void, Result> {
+        @Override
+        protected Result doInBackground(Void... params) {
+            final ContentResolver resolver = getContext().getContentResolver();
+            final Uri uriCurrentFormat = convertLegacyIfNecessary(mLookupUri);
+
+            // Handle both legacy and new authorities
+
+            final long contactId;
+            final String selection = "0";
+            if (Contacts.CONTENT_ITEM_TYPE.equals(mMimeType)) {
+                // Handle selected aggregate
+                contactId = ContentUris.parseId(uriCurrentFormat);
+            } else if (RawContacts.CONTENT_ITEM_TYPE.equals(mMimeType)) {
+                // Get id of corresponding aggregate
+                final long rawContactId = ContentUris.parseId(uriCurrentFormat);
+                contactId = ContactsUtils.queryForContactId(resolver, rawContactId);
+            } else throw new IllegalStateException();
+
+            return new Result(EntitySet.fromQuery(resolver, RawContacts.CONTACT_ID + "=?",
+                    new String[] { String.valueOf(contactId) }, null));
+        }
+
+        /**
+         * Transforms the given Uri and returns a Lookup-Uri that represents the contact.
+         * For legacy contacts, a raw-contact lookup is performed.
+         */
+        private Uri convertLegacyIfNecessary(Uri uri) {
+            if (uri == null) throw new IllegalArgumentException("uri must not be null");
+
+            final String authority = uri.getAuthority();
+
+            // Current Style Uri? Just return it
+            if (ContactsContract.AUTHORITY.equals(authority)) {
+                return uri;
+            }
+
+            // Legacy Style? Convert to RawContact
+            final String OBSOLETE_AUTHORITY = "contacts";
+            if (OBSOLETE_AUTHORITY.equals(authority)) {
+                // Legacy Format. Convert to RawContact-Uri and then lookup the contact
+                final long rawContactId = ContentUris.parseId(uri);
+                return RawContacts.getContactLookupUri(getContext().getContentResolver(),
+                        ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
+            }
+
+            throw new IllegalArgumentException("uri format is unknown");
+        }
+
+        @Override
+        protected void onPostExecute(Result result) {
+            super.onPostExecute(result);
+
+            // TODO: This merging of extras is probably wrong on subsequent loads
+
+            // Load edit details in background
+            final Sources sources = Sources.getInstance(getContext());
+
+            // Handle any incoming values that should be inserted
+            final boolean hasExtras = mIntentExtras != null && mIntentExtras.size() > 0;
+            final boolean hasState = result.getEntitySet().size() > 0;
+            if (hasExtras && hasState) {
+                // Find source defining the first RawContact found
+                final EntityDelta state = result.getEntitySet().get(0);
+                final String accountType = state.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+                final ContactsSource source = sources.getInflatedSource(accountType,
+                        ContactsSource.LEVEL_CONSTRAINTS);
+                EntityModifier.parseExtras(getContext(), source, state, mIntentExtras);
+            }
+
+            // The creator isn't interested in any further updates
+            if (mDestroyed) {
+                return;
+            }
+
+            mContact = result;
+            if (result != null) {
+                if (mObserver == null) {
+                    mObserver = new ForceLoadContentObserver();
+                }
+                // TODO: Do we want a content observer here?
+//                Log.i(TAG, "Registering content observer for " + mLookupUri);
+//                getContext().getContentResolver().registerContentObserver(mLookupUri, true,
+//                        mObserver);
+                deliverResult(result);
+            }
+        }
+    }
+
+    @Override
+    public void startLoading() {
+        if (mContact != null) {
+            deliverResult(mContact);
+        } else {
+            forceLoad();
+        }
+    }
+
+    @Override
+    public void forceLoad() {
+        LoadContactTask task = new LoadContactTask();
+        task.execute((Void[])null);
+    }
+
+    @Override
+    public void stopLoading() {
+        mContact = null;
+        if (mObserver != null) {
+            getContext().getContentResolver().unregisterContentObserver(mObserver);
+        }
+    }
+
+    @Override
+    public void destroy() {
+        mContact = null;
+        mDestroyed = true;
+    }
+}
diff --git a/src/com/android/contacts/widget/CompositeListAdapter.java b/src/com/android/contacts/widget/CompositeListAdapter.java
new file mode 100644
index 0000000..bd0c8d6
--- /dev/null
+++ b/src/com/android/contacts/widget/CompositeListAdapter.java
@@ -0,0 +1,222 @@
+/*
+ * 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.widget;
+
+import android.database.DataSetObserver;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListAdapter;
+
+/**
+ * A general purpose adapter that is composed of multiple sub-adapters. It just
+ * appends them in the order they are added. It listens to changes from all
+ * sub-adapters and propagates them to its own listeners.
+ */
+public class CompositeListAdapter extends BaseAdapter {
+
+    private static final int INITIAL_CAPACITY = 2;
+
+    private ListAdapter[] mAdapters;
+    private int[] mCounts;
+    private int[] mViewTypeCounts;
+    private int mSize = 0;
+    private int mCount = 0;
+    private int mViewTypeCount = 0;
+    private boolean mAllItemsEnabled = true;
+    private boolean mCacheValid = true;
+
+    private DataSetObserver mDataSetObserver = new DataSetObserver() {
+
+        @Override
+        public void onChanged() {
+            invalidate();
+            notifyDataChanged();
+        }
+
+        @Override
+        public void onInvalidated() {
+            invalidate();
+            notifyDataChanged();
+        }
+    };
+
+    public CompositeListAdapter() {
+        this(INITIAL_CAPACITY);
+    }
+
+    public CompositeListAdapter(int initialCapacity) {
+        mAdapters = new ListAdapter[INITIAL_CAPACITY];
+        mCounts = new int[INITIAL_CAPACITY];
+        mViewTypeCounts = new int[INITIAL_CAPACITY];
+    }
+
+    public void addAdapter(ListAdapter adapter) {
+        if (mSize >= mAdapters.length) {
+            int newCapacity = mSize + 2;
+            ListAdapter[] newAdapters = new ListAdapter[newCapacity];
+            System.arraycopy(mAdapters, 0, newAdapters, 0, mSize);
+            mAdapters = newAdapters;
+
+            int[] newCounts = new int[newCapacity];
+            System.arraycopy(mCounts, 0, newCounts, 0, mSize);
+            mCounts = newCounts;
+
+            int[] newViewTypeCounts = new int[newCapacity];
+            System.arraycopy(mViewTypeCounts, 0, newViewTypeCounts, 0, mSize);
+            mViewTypeCounts = newViewTypeCounts;
+        }
+
+        adapter.registerDataSetObserver(mDataSetObserver);
+
+        int count = adapter.getCount();
+        int viewTypeCount = adapter.getViewTypeCount();
+
+        mAdapters[mSize] = adapter;
+        mCounts[mSize] = count;
+        mCount += count;
+        mAllItemsEnabled &= adapter.areAllItemsEnabled();
+        mViewTypeCounts[mSize] = viewTypeCount;
+        mViewTypeCount += viewTypeCount;
+        mSize++;
+
+        notifyDataChanged();
+    }
+
+    protected void notifyDataChanged() {
+        if (getCount() > 0) {
+            notifyDataSetChanged();
+        } else {
+            notifyDataSetInvalidated();
+        }
+    }
+
+    protected void invalidate() {
+        mCacheValid = false;
+    }
+
+    protected void ensureCacheValid() {
+        if (mCacheValid) {
+            return;
+        }
+
+        mCount = 0;
+        mAllItemsEnabled = true;
+        mViewTypeCount = 0;
+        for (int i = 0; i < mSize; i++) {
+            int count = mAdapters[i].getCount();
+            int viewTypeCount = mAdapters[i].getViewTypeCount();
+            mCounts[i] = count;
+            mCount += count;
+            mAllItemsEnabled &= mAdapters[i].areAllItemsEnabled();
+            mViewTypeCount += viewTypeCount;
+        }
+
+        mCacheValid = true;
+    }
+
+    public int getCount() {
+        ensureCacheValid();
+        return mCount;
+    }
+
+    public Object getItem(int position) {
+        ensureCacheValid();
+        int start = 0;
+        for (int i = 0; i < mCounts.length; i++) {
+            int end = start + mCounts[i];
+            if (position >= start && position < end) {
+                return mAdapters[i].getItem(position - start);
+            }
+            start = end;
+        }
+
+        throw new ArrayIndexOutOfBoundsException(position);
+    }
+
+    public long getItemId(int position) {
+        ensureCacheValid();
+        int start = 0;
+        for (int i = 0; i < mCounts.length; i++) {
+            int end = start + mCounts[i];
+            if (position >= start && position < end) {
+                return mAdapters[i].getItemId(position - start);
+            }
+            start = end;
+        }
+
+        throw new ArrayIndexOutOfBoundsException(position);
+    }
+
+    @Override
+    public int getViewTypeCount() {
+        ensureCacheValid();
+        return mViewTypeCount;
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        ensureCacheValid();
+        int start = 0;
+        int viewTypeOffset = 0;
+        for (int i = 0; i < mCounts.length; i++) {
+            int end = start + mCounts[i];
+            if (position >= start && position < end) {
+                return viewTypeOffset + mAdapters[i].getItemViewType(position - start);
+            }
+            viewTypeOffset += mViewTypeCounts[i];
+            start = end;
+        }
+
+        throw new ArrayIndexOutOfBoundsException(position);
+    }
+
+    public View getView(int position, View convertView, ViewGroup parent) {
+        ensureCacheValid();
+        int start = 0;
+        for (int i = 0; i < mCounts.length; i++) {
+            int end = start + mCounts[i];
+            if (position >= start && position < end) {
+                return mAdapters[i].getView(position - start, convertView, parent);
+            }
+            start = end;
+        }
+
+        throw new ArrayIndexOutOfBoundsException(position);
+    }
+
+    @Override
+    public boolean areAllItemsEnabled() {
+        ensureCacheValid();
+        return mAllItemsEnabled;
+    }
+
+    @Override
+    public boolean isEnabled(int position) {
+        ensureCacheValid();
+        int start = 0;
+        for (int i = 0; i < mCounts.length; i++) {
+            int end = start + mCounts[i];
+            if (position >= start && position < end) {
+                return mAdapters[i].areAllItemsEnabled()
+                        || mAdapters[i].isEnabled(position - start);
+            }
+            start = end;
+        }
+
+        throw new ArrayIndexOutOfBoundsException(position);
+    }
+}
diff --git a/src/com/android/contacts/widget/ContextMenuAdapter.java b/src/com/android/contacts/widget/ContextMenuAdapter.java
new file mode 100644
index 0000000..660274a
--- /dev/null
+++ b/src/com/android/contacts/widget/ContextMenuAdapter.java
@@ -0,0 +1,30 @@
+/*
+ * 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.widget;
+
+import android.view.MenuItem;
+import android.view.View;
+
+/**
+ * An adapter for the contextual menu.
+ */
+public interface ContextMenuAdapter extends View.OnCreateContextMenuListener {
+
+    /**
+     * See {@link android.app.Activity#onContextItemSelected}.
+     */
+    boolean onContextItemSelected(MenuItem item);
+}
diff --git a/src/com/android/contacts/widget/PinnedHeaderListAdapter.java b/src/com/android/contacts/widget/PinnedHeaderListAdapter.java
new file mode 100644
index 0000000..7492de1
--- /dev/null
+++ b/src/com/android/contacts/widget/PinnedHeaderListAdapter.java
@@ -0,0 +1,162 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CursorAdapter;
+import android.widget.SectionIndexer;
+import android.widget.TextView;
+
+/**
+ * A list adapter that supports section indexer and a pinned header.
+ */
+public abstract class PinnedHeaderListAdapter extends CursorAdapter
+        implements SectionIndexer, PinnedHeaderListView.PinnedHeaderAdapter {
+
+    private final int mPinnedHeaderBackgroundColor;
+    private final int mSectionHeaderTextViewId;
+    private final int mSectionHeaderLayoutResId;
+
+    private SectionIndexer mIndexer;
+
+    /**
+     * Constructor.
+     *
+     * @param context
+     * @param sectionHeaderLayoutResourceId section header layout resource ID
+     * @param sectionHeaderTextViewId section header text view ID
+     * @param backgroundColor An approximation of the background color of the
+     *            pinned header. This color is used when the pinned header is
+     *            being pushed up. At that point the header "fades away". Rather
+     *            than computing a faded bitmap based on the 9-patch normally
+     *            used for the background, we will use a solid color, which will
+     *            provide better performance and reduced complexity.
+     */
+    public PinnedHeaderListAdapter(Context context, int sectionHeaderLayoutResourceId,
+            int sectionHeaderTextViewId, int backgroundColor) {
+        super(context, null, false);
+        this.mContext = context;
+        mPinnedHeaderBackgroundColor = backgroundColor;
+        mSectionHeaderLayoutResId = sectionHeaderLayoutResourceId;
+        mSectionHeaderTextViewId = sectionHeaderTextViewId;
+    }
+
+    public void setIndexer(SectionIndexer indexer) {
+        mIndexer = indexer;
+    }
+
+    public Object [] getSections() {
+        if (mIndexer == null) {
+            return new String[] { " " };
+        } else {
+            return mIndexer.getSections();
+        }
+    }
+
+    public int getPositionForSection(int sectionIndex) {
+        if (mIndexer == null) {
+            return -1;
+        }
+
+        return mIndexer.getPositionForSection(sectionIndex);
+    }
+
+    public int getSectionForPosition(int position) {
+        if (mIndexer == null) {
+            return -1;
+        }
+
+        return mIndexer.getSectionForPosition(position);
+    }
+
+    /**
+     * Computes the state of the pinned header.  It can be invisible, fully
+     * visible or partially pushed up out of the view.
+     */
+    public int getPinnedHeaderState(int position) {
+        if (mIndexer == null || mCursor == null || mCursor.getCount() == 0) {
+            return PINNED_HEADER_GONE;
+        }
+
+        // The header should get pushed up if the top item shown
+        // is the last item in a section for a particular letter.
+        int section = getSectionForPosition(position);
+        if (section == -1) {
+            return PINNED_HEADER_GONE;
+        }
+
+        int nextSectionPosition = getPositionForSection(section + 1);
+        if (nextSectionPosition != -1 && position == nextSectionPosition - 1) {
+            return PINNED_HEADER_PUSHED_UP;
+        }
+
+        return PINNED_HEADER_VISIBLE;
+    }
+
+    final static class PinnedHeaderCache {
+        public TextView titleView;
+        public ColorStateList textColor;
+        public Drawable background;
+    }
+
+    /**
+     * Configures the pinned header by setting the appropriate text label
+     * and also adjusting color if necessary.  The color needs to be
+     * adjusted when the pinned header is being pushed up from the view.
+     */
+    public void configurePinnedHeader(View header, int position, int alpha) {
+        PinnedHeaderCache cache = (PinnedHeaderCache)header.getTag();
+        if (cache == null) {
+            cache = new PinnedHeaderCache();
+            cache.titleView = (TextView)header.findViewById(mSectionHeaderTextViewId);
+            cache.textColor = cache.titleView.getTextColors();
+            cache.background = header.getBackground();
+            header.setTag(cache);
+        }
+
+        int section = getSectionForPosition(position);
+
+        String title = (String)mIndexer.getSections()[section];
+        cache.titleView.setText(title);
+
+        if (alpha == 255) {
+            // Opaque: use the default background, and the original text color
+            header.setBackgroundDrawable(cache.background);
+            cache.titleView.setTextColor(cache.textColor);
+        } else {
+            // Faded: use a solid color approximation of the background, and
+            // a translucent text color
+            header.setBackgroundColor(Color.rgb(
+                    Color.red(mPinnedHeaderBackgroundColor) * alpha / 255,
+                    Color.green(mPinnedHeaderBackgroundColor) * alpha / 255,
+                    Color.blue(mPinnedHeaderBackgroundColor) * alpha / 255));
+
+            int textColor = cache.textColor.getDefaultColor();
+            cache.titleView.setTextColor(Color.argb(alpha,
+                    Color.red(textColor), Color.green(textColor), Color.blue(textColor)));
+        }
+    }
+
+    public View createPinnedHeaderView(ViewGroup parent) {
+        return LayoutInflater.from(mContext).inflate(mSectionHeaderLayoutResId, parent, false);
+    }
+}
diff --git a/src/com/android/contacts/PinnedHeaderListView.java b/src/com/android/contacts/widget/PinnedHeaderListView.java
similarity index 68%
rename from src/com/android/contacts/PinnedHeaderListView.java
rename to src/com/android/contacts/widget/PinnedHeaderListView.java
index 9d1391b..5893ac6 100644
--- a/src/com/android/contacts/PinnedHeaderListView.java
+++ b/src/com/android/contacts/widget/PinnedHeaderListView.java
@@ -14,20 +14,23 @@
  * limitations under the License.
  */
 
-package com.android.contacts;
+package com.android.contacts.widget;
 
 import android.content.Context;
 import android.graphics.Canvas;
 import android.util.AttributeSet;
 import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
 import android.widget.ListAdapter;
 import android.widget.ListView;
+import android.widget.AbsListView.OnScrollListener;
 
 /**
  * A ListView that maintains a header pinned at the top of the list. The
  * pinned header can be pushed up and dissolved as needed.
  */
-public class PinnedHeaderListView extends ListView {
+public class PinnedHeaderListView extends ListView implements OnScrollListener {
 
     /**
      * Adapter interface.  The list adapter must implement this interface.
@@ -51,6 +54,11 @@
         public static final int PINNED_HEADER_PUSHED_UP = 2;
 
         /**
+         * Creates the pinned header view.
+         */
+        View createPinnedHeaderView(ViewGroup parent);
+
+        /**
          * Computes the desired state of the pinned header for the given
          * position of the first visible list item. Allowed return values are
          * {@link #PINNED_HEADER_GONE}, {@link #PINNED_HEADER_VISIBLE} or
@@ -73,21 +81,26 @@
     private PinnedHeaderAdapter mAdapter;
     private View mHeaderView;
     private boolean mHeaderViewVisible;
-
+    private float mHeaderOffset;
     private int mHeaderViewWidth;
-
     private int mHeaderViewHeight;
 
+    private OnScrollListener mOnScrollListener;
+
+    private boolean mHeaderViewConfigured;
+
     public PinnedHeaderListView(Context context) {
         super(context);
     }
 
     public PinnedHeaderListView(Context context, AttributeSet attrs) {
         super(context, attrs);
+        super.setOnScrollListener(this);
     }
 
     public PinnedHeaderListView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
+        super.setOnScrollListener(this);
     }
 
     public void setPinnedHeaderView(View view) {
@@ -99,6 +112,7 @@
         if (mHeaderView != null) {
             setFadingEdgeLength(0);
         }
+        mHeaderViewConfigured = false;
         requestLayout();
     }
 
@@ -106,12 +120,23 @@
     public void setAdapter(ListAdapter adapter) {
         super.setAdapter(adapter);
         mAdapter = (PinnedHeaderAdapter)adapter;
+        View headerView = mAdapter.createPinnedHeaderView(this);
+        setPinnedHeaderView(headerView);
+    }
+
+    @Override
+    public void setOnScrollListener(OnScrollListener onScrollListener) {
+        mOnScrollListener = onScrollListener;
+        super.setOnScrollListener(this);
     }
 
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
         if (mHeaderView != null) {
+            if (!mHeaderViewConfigured) {
+                configureHeaderView(getFirstVisiblePosition() - getHeaderViewsCount());
+            }
             measureChild(mHeaderView, widthMeasureSpec, heightMeasureSpec);
             mHeaderViewWidth = mHeaderView.getMeasuredWidth();
             mHeaderViewHeight = mHeaderView.getMeasuredHeight();
@@ -122,8 +147,24 @@
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
         if (mHeaderView != null) {
+            if (!mHeaderViewConfigured) {
+                configureHeaderView(getFirstVisiblePosition() - getHeaderViewsCount());
+            }
             mHeaderView.layout(0, 0, mHeaderViewWidth, mHeaderViewHeight);
-            configureHeaderView(getFirstVisiblePosition());
+        }
+    }
+
+    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+            int totalItemCount) {
+        configureHeaderView(firstVisibleItem - getHeaderViewsCount());
+        if (mOnScrollListener != null) {
+            mOnScrollListener.onScroll(this, firstVisibleItem, visibleItemCount, totalItemCount);
+        }
+    }
+
+    public void onScrollStateChanged(AbsListView view, int scrollState) {
+        if (mOnScrollListener != null) {
+            mOnScrollListener.onScrollStateChanged(this, scrollState);
         }
     }
 
@@ -141,42 +182,40 @@
 
             case PinnedHeaderAdapter.PINNED_HEADER_VISIBLE: {
                 mAdapter.configurePinnedHeader(mHeaderView, position, MAX_ALPHA);
-                if (mHeaderView.getTop() != 0) {
-                    mHeaderView.layout(0, 0, mHeaderViewWidth, mHeaderViewHeight);
-                }
+                mHeaderOffset = 0;
                 mHeaderViewVisible = true;
                 break;
             }
 
             case PinnedHeaderAdapter.PINNED_HEADER_PUSHED_UP: {
-                View firstView = getChildAt(0);
-                int bottom = firstView.getBottom();
-                int itemHeight = firstView.getHeight();
-                int headerHeight = mHeaderView.getHeight();
-                int y;
-                int alpha;
-                if (bottom < headerHeight) {
-                    y = (bottom - headerHeight);
-                    alpha = MAX_ALPHA * (headerHeight + y) / headerHeight;
-                } else {
-                    y = 0;
-                    alpha = MAX_ALPHA;
+                int y = 0;
+                int alpha = MAX_ALPHA;
+                if (getChildCount() != 0) {
+                    View firstView = getChildAt(0);
+                    int bottom = firstView.getBottom();
+                    int headerHeight = mHeaderViewHeight;
+                    if (bottom < headerHeight) {
+                        y = (bottom - headerHeight);
+                        alpha = MAX_ALPHA * (headerHeight + y) / headerHeight;
+                    }
                 }
                 mAdapter.configurePinnedHeader(mHeaderView, position, alpha);
-                if (mHeaderView.getTop() != y) {
-                    mHeaderView.layout(0, y, mHeaderViewWidth, mHeaderViewHeight + y);
-                }
+                mHeaderOffset = y;
                 mHeaderViewVisible = true;
                 break;
             }
         }
+        mHeaderViewConfigured = true;
     }
 
     @Override
     protected void dispatchDraw(Canvas canvas) {
         super.dispatchDraw(canvas);
         if (mHeaderViewVisible) {
+            canvas.save();
+            canvas.translate(0, mHeaderOffset);
             drawChild(canvas, mHeaderView, getDrawingTime());
+            canvas.restore();
         }
     }
 }
diff --git a/src/com/android/contacts/widget/SearchEditText.java b/src/com/android/contacts/widget/SearchEditText.java
new file mode 100644
index 0000000..45001a5
--- /dev/null
+++ b/src/com/android/contacts/widget/SearchEditText.java
@@ -0,0 +1,130 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+
+/**
+ * A custom text editor that helps automatically dismiss the activity along with the soft
+ * keyboard.
+ */
+public class SearchEditText extends EditText implements OnEditorActionListener, TextWatcher {
+    private boolean mMagnifyingGlassShown = true;
+
+    private Drawable mMagnifyingGlass;
+    private OnFilterTextListener mListener;
+
+    public interface OnFilterTextListener {
+        void onFilterChange(String queryString);
+        void onCancelSearch();
+    }
+
+    public SearchEditText(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        addTextChangedListener(this);
+        setOnEditorActionListener(this);
+        mMagnifyingGlass = getCompoundDrawables()[2];
+    }
+
+    public void setOnFilterTextListener(OnFilterTextListener listener) {
+        this.mListener = listener;
+    }
+
+    /**
+     * Conditionally shows a magnifying glass icon on the right side of the text field
+     * when the text it empty.
+     */
+    @Override
+    public boolean onPreDraw() {
+        boolean emptyText = TextUtils.isEmpty(getText());
+        if (mMagnifyingGlassShown != emptyText) {
+            mMagnifyingGlassShown = emptyText;
+            if (mMagnifyingGlassShown) {
+                setCompoundDrawables(null, null, mMagnifyingGlass, null);
+            } else {
+                setCompoundDrawables(null, null, null, null);
+            }
+            return false;
+        }
+        return super.onPreDraw();
+    }
+
+    /**
+     * Dismisses the search UI along with the keyboard if the filter text is empty.
+     */
+    @Override
+    public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+        if (keyCode == KeyEvent.KEYCODE_BACK && TextUtils.isEmpty(getText()) && mListener != null) {
+            mListener.onCancelSearch();
+            return true;
+        }
+        return false;
+    }
+
+    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+    }
+
+    @Override
+    public void onTextChanged(CharSequence s, int start, int before, int count) {
+    }
+
+    /**
+     * Event handler for search UI.
+     */
+    public void afterTextChanged(Editable s) {
+        if (mListener != null) {
+            mListener.onFilterChange(trim(s));
+        }
+    }
+
+    private String trim(Editable s) {
+        return s.toString().trim();
+    }
+
+    /**
+     * Event handler for search UI.
+     */
+    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+        if (actionId == EditorInfo.IME_ACTION_DONE) {
+            hideSoftKeyboard();
+            if (TextUtils.isEmpty(trim(getText())) && mListener != null) {
+                mListener.onCancelSearch();
+            }
+            return true;
+        }
+        return false;
+    }
+
+    private void hideSoftKeyboard() {
+        // Hide soft keyboard, if visible
+        InputMethodManager inputMethodManager = (InputMethodManager)
+                getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+        inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
+    }
+
+}
diff --git a/src/com/android/contacts/widget/SingleItemAdapter.java b/src/com/android/contacts/widget/SingleItemAdapter.java
new file mode 100644
index 0000000..3532bfc
--- /dev/null
+++ b/src/com/android/contacts/widget/SingleItemAdapter.java
@@ -0,0 +1,47 @@
+/*
+ * 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.widget;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+/**
+ * A general purpose adapter that contains exactly one item.
+ */
+public abstract class SingleItemAdapter extends BaseAdapter {
+
+    public int getCount() {
+        return 1;
+    }
+
+    public Object getItem(int position) {
+        return null;
+    }
+
+    public long getItemId(int position) {
+        return 0;
+    }
+
+    public View getView(int position, View convertView, ViewGroup parent) {
+        return getView(convertView, parent);
+    }
+
+    /**
+     * Creates the view.
+     */
+    protected abstract View getView(View convertView, ViewGroup parent);
+}
diff --git a/src/com/android/contacts/TextHighlightingAnimation.java b/src/com/android/contacts/widget/TextHighlightingAnimation.java
similarity index 96%
rename from src/com/android/contacts/TextHighlightingAnimation.java
rename to src/com/android/contacts/widget/TextHighlightingAnimation.java
index e35ae1e..049e5cd 100644
--- a/src/com/android/contacts/TextHighlightingAnimation.java
+++ b/src/com/android/contacts/widget/TextHighlightingAnimation.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.contacts;
+package com.android.contacts.widget;
 
 import com.android.internal.R;
 
@@ -29,7 +29,7 @@
 /**
  * An animation that alternately dims and brightens the non-highlighted portion of text.
  */
-public abstract class TextHighlightingAnimation implements Runnable {
+public abstract class TextHighlightingAnimation implements Runnable, TextWithHighlightingFactory {
 
     private static final int MAX_ALPHA = 255;
     private static final int MIN_ALPHA = 50;
@@ -54,7 +54,7 @@
     /**
      * A Spanned that highlights a part of text by dimming another part of that text.
      */
-    public class TextWithHighlighting implements Spanned {
+    public class TextWithHighlightingImpl implements TextWithHighlighting {
 
         private final DimmingSpan[] mSpans;
         private boolean mDimmingEnabled;
@@ -63,7 +63,7 @@
         private int mDimmingSpanEnd;
         private String mString;
 
-        public TextWithHighlighting() {
+        public TextWithHighlightingImpl() {
             mSpans = new DimmingSpan[] { mDimmingSpan };
         }
 
@@ -216,12 +216,12 @@
     /**
      * Returns a Spanned that can be used by a text view to show text with highlighting.
      */
-    public TextWithHighlighting createTextWithHighlighting() {
-        return new TextWithHighlighting();
+    public TextWithHighlightingImpl createTextWithHighlighting() {
+        return new TextWithHighlightingImpl();
     }
 
     /**
-     * Override and invalidate (redraw) TextViews showing {@link TextWithHighlighting}.
+     * Override and invalidate (redraw) TextViews showing {@link TextWithHighlightingImpl}.
      */
     protected abstract void invalidate();
 
diff --git a/src/com/android/contacts/widget/TextWithHighlighting.java b/src/com/android/contacts/widget/TextWithHighlighting.java
new file mode 100644
index 0000000..3a32b02
--- /dev/null
+++ b/src/com/android/contacts/widget/TextWithHighlighting.java
@@ -0,0 +1,26 @@
+/*
+ * 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.widget;
+
+import android.database.CharArrayBuffer;
+import android.text.Spanned;
+
+/**
+ * A Spanned that highlights a part of text by dimming another part of that text.
+ */
+public interface TextWithHighlighting extends Spanned {
+    void setText(CharArrayBuffer baseText, CharArrayBuffer highlightedText);
+}
diff --git a/src/com/android/contacts/widget/TextWithHighlightingFactory.java b/src/com/android/contacts/widget/TextWithHighlightingFactory.java
new file mode 100644
index 0000000..ee5744d
--- /dev/null
+++ b/src/com/android/contacts/widget/TextWithHighlightingFactory.java
@@ -0,0 +1,23 @@
+/*
+ * 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.widget;
+
+/**
+ * A factory for text fields with animated highlighting.
+ */
+public interface TextWithHighlightingFactory {
+    TextWithHighlighting createTextWithHighlighting();
+}
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 7af1a54..17ad3f7 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -4,9 +4,9 @@
      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.
@@ -17,16 +17,39 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.contacts.tests">
 
+    <uses-permission android:name="android.permission.READ_CONTACTS" />
+
     <application>
         <uses-library android:name="android.test.runner" />
         <meta-data android:name="com.android.contacts.iconset" android:resource="@xml/iconset" />
+        <uses-permission android:name="android.permission.READ_CONTACTS" />
+
+        <activity android:name=".allintents.AllIntentsActivity"
+            android:label="@string/contactsIntents"
+            >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+        <activity android:name=".allintents.ResultActivity"
+            android:label="@string/result"
+            >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
     </application>
 
     <instrumentation android:name="android.test.InstrumentationTestRunner"
         android:targetPackage="com.android.contacts"
         android:label="Contacts app tests">
     </instrumentation>
-    
+
     <instrumentation android:name="com.android.contacts.ContactsLaunchPerformance"
         android:targetPackage="com.android.contacts"
         android:label="Contacts launch performance">
@@ -38,4 +61,4 @@
         android:label="Dialer launch performance">
     </instrumentation>
 
-</manifest> 
+</manifest>
diff --git a/tests/res/layout/intent_list_item.xml b/tests/res/layout/intent_list_item.xml
new file mode 100644
index 0000000..4749224
--- /dev/null
+++ b/tests/res/layout/intent_list_item.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2006 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.
+-->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@android:id/text1"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:textAppearance="?android:attr/textAppearanceSmall"
+    android:gravity="center_vertical"
+    android:paddingLeft="6dip"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+/>
diff --git a/tests/res/layout/result.xml b/tests/res/layout/result.xml
new file mode 100644
index 0000000..0ab32c6
--- /dev/null
+++ b/tests/res/layout/result.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<ScrollView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:fillViewport="true"
+>
+
+  <TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
+      android:id="@+id/table"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"
+      android:shrinkColumns="1"
+      android:stretchColumns="*">
+  </TableLayout>
+</ScrollView>
+
diff --git a/tests/res/values/donottranslate_strings.xml b/tests/res/values/donottranslate_strings.xml
new file mode 100644
index 0000000..745b351
--- /dev/null
+++ b/tests/res/values/donottranslate_strings.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+    <string name="contactsIntents">Contacts Intents</string>
+    <string name="result">Result returned by activity</string>
+
+    <string-array name="allIntents">
+        <!-- List modes -->
+        <item>LIST_DEFAULT</item>
+        <item>LIST_ALL_CONTACTS_ACTION</item>
+        <item>LIST_CONTACTS_WITH_PHONES_ACTION</item>
+        <item>LIST_STARRED_ACTION</item>
+        <item>LIST_STARRED_ACTION (filter)</item>
+        <item>LIST_FREQUENT_ACTION</item>
+        <item>LIST_FREQUENT_ACTION (filter)</item>
+        <item>LIST_STREQUENT_ACTION</item>
+        <item>LIST_STREQUENT_ACTION (filter)</item>
+        <item>ACTION_PICK: contact</item>
+        <item>ACTION_PICK: contact (legacy)</item>
+        <item>ACTION_PICK: phone</item>
+        <item>ACTION_PICK: phone (legacy)</item>
+        <item>ACTION_PICK: postal</item>
+        <item>ACTION_PICK: postal (legacy)</item>
+        <item>ACTION_CREATE_SHORTCUT: contact</item>
+        <item>ACTION_CREATE_SHORTCUT: contact (filter)</item>
+        <item>ACTION_CREATE_SHORTCUT: dial</item>
+        <item>ACTION_CREATE_SHORTCUT: dial (filter)</item>
+        <item>ACTION_CREATE_SHORTCUT: message</item>
+        <item>ACTION_CREATE_SHORTCUT: message (filter)</item>
+        <item>ACTION_GET_CONTENT: contact</item>
+        <item>ACTION_GET_CONTENT: contact (filter)</item>
+        <item>ACTION_GET_CONTENT: contact (legacy)</item>
+        <item>ACTION_GET_CONTENT: contact (filter, legacy)</item>
+        <item>ACTION_GET_CONTENT: phone</item>
+        <item>ACTION_GET_CONTENT: phone (filter)</item>
+        <item>ACTION_GET_CONTENT: phone (legacy)</item>
+        <item>ACTION_GET_CONTENT: postal</item>
+        <item>ACTION_GET_CONTENT: postal (filter)</item>
+        <item>ACTION_GET_CONTENT: postal (legacy)</item>
+        <item>ACTION_INSERT_OR_EDIT</item>
+        <item>ACTION_SEARCH (call button)</item>
+        <item>ACTION_SEARCH: contact</item>
+        <item>ACTION_SEARCH: email</item>
+        <item>ACTION_SEARCH: phone</item>
+        <item>SEARCH_SUGGESTION_CLICKED (call button)</item>
+        <item>SEARCH_SUGGESTION_CLICKED: contact</item>
+        <item>SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED</item>
+        <item>SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED</item>
+        <item>TODO: JOIN_CONTACT</item>
+        <item>ACTION_GET_MULTIPLE_PHONES</item>
+
+        <!-- Edit Contact -->
+        <item>EDIT (content uri with only id)</item>
+        <item>EDIT (lookup uri without id)</item>
+        <item>EDIT (lookup uri)</item>
+        <item>EDIT (called for raw contact)</item>
+        <item>EDIT (legacy style uri)</item>
+        <item>EDIT (create new contact)</item>
+        <item>EDIT (create new raw contact)</item>
+        <item>EDIT (create new legacy)</item>
+    </string-array>
+</resources>
diff --git a/tests/src/com/android/contacts/ContactDetailLoaderTest.java b/tests/src/com/android/contacts/ContactDetailLoaderTest.java
new file mode 100644
index 0000000..77cfb1f
--- /dev/null
+++ b/tests/src/com/android/contacts/ContactDetailLoaderTest.java
@@ -0,0 +1,538 @@
+/*
+ * 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.tests.mocks.ContactsMockContext;
+import com.android.contacts.tests.mocks.MockContentProvider;
+import com.android.contacts.views.detail.ContactDetailLoader;
+
+import android.app.patterns.Loader;
+import android.app.patterns.Loader.OnLoadCompleteListener;
+import android.content.ContentUris;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.RawContactsEntity;
+import android.provider.ContactsContract.StatusUpdates;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.RawContacts.Data;
+import android.provider.ContactsContract.RawContacts.Entity;
+import android.test.AndroidTestCase;
+import android.test.AssertionFailedError;
+import android.test.suitebuilder.annotation.Suppress;
+
+import java.util.concurrent.ArrayBlockingQueue;
+
+/**
+ * Runs ContactLoader tests for the the contact-detail view.
+ */
+public class ContactDetailLoaderTest extends AndroidTestCase {
+    private ContactsMockContext mMockContext;
+    private MockContentProvider mContactsProvider;
+
+    static {
+        // Need to force class loading of AsyncTask on the main thread...
+        new AsyncTask<Void, Void, Void>() {
+            @Override
+            protected Void doInBackground(Void... args) {return null;}
+            @Override
+            protected void onPostExecute(Void result) {}
+        };
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mMockContext = new ContactsMockContext(getContext());
+        mContactsProvider = mMockContext.getContactsProvider();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    /**
+     * Utility function to ensure that an Exception is thrown during the code
+     * TODO: This should go to MoreAsserts at one point
+     */
+    @SuppressWarnings("unchecked")
+    private static <E extends Throwable> E assertThrows(
+            Class<E> expectedException, Runnable runnable) {
+        try {
+            runnable.run();
+        } catch (Throwable exception) {
+            Class<? extends Throwable> receivedException = exception.getClass();
+            if (expectedException == receivedException) return (E) exception;
+            throw new AssertionFailedError("Expected Exception " + expectedException +
+                    " but " + receivedException + " was thrown. Details: " + exception);
+        }
+        throw new AssertionFailedError(
+                "Expected Exception " + expectedException + " which was not thrown");
+    }
+
+    /**
+     * Runs a Loader synchronously and returns the result of the load. The loader will
+     * be started, stopped, and destroyed by this method so it cannot be reused.
+     *
+     * @param loader The loader to run synchronously
+     * @return The result from the loader
+     */
+    private <T> T getLoaderResultSynchronously(final Loader<T> loader) {
+        // The test thread blocks on this queue until the loader puts it's result in
+        final ArrayBlockingQueue<T> queue = new ArrayBlockingQueue<T>(1);
+
+        // This callback runs on the "main" thread and unblocks the test thread
+        // when it puts the result into the blocking queue
+        final OnLoadCompleteListener<T> listener = new OnLoadCompleteListener<T>() {
+            public void onLoadComplete(Loader<T> completedLoader, T data) {
+                // Shut the loader down
+                completedLoader.unregisterListener(this);
+                completedLoader.stopLoading();
+                completedLoader.destroy();
+
+                // Store the result, unblocking the test thread
+                queue.add(data);
+            }
+        };
+
+        // This handler runs on the "main" thread of the process since AsyncTask
+        // is documented as needing to run on the main thread and many Loaders use
+        // AsyncTask
+        final Handler mainThreadHandler = new Handler(Looper.getMainLooper()) {
+            @Override
+            public void handleMessage(Message msg) {
+                loader.registerListener(0, listener);
+                loader.startLoading();
+            }
+        };
+
+        // Ask the main thread to start the loading process
+        mainThreadHandler.sendEmptyMessage(0);
+
+        // Block on the queue waiting for the result of the load to be inserted
+        T result;
+        while (true) {
+            try {
+                result = queue.take();
+                break;
+            } catch (InterruptedException e) {
+                throw new RuntimeException("waiting thread interrupted", e);
+            }
+        }
+
+        return result;
+    }
+
+    private ContactDetailLoader.Result assertLoadContact(Uri uri) {
+        final ContactDetailLoader loader = new ContactDetailLoader(mMockContext, uri);
+        return getLoaderResultSynchronously(loader);
+    }
+
+    public void testNullUri() {
+        ContactDetailLoader.Result result = assertLoadContact(null);
+        assertEquals(ContactDetailLoader.Result.ERROR, result);
+    }
+
+    public void testEmptyUri() {
+        ContactDetailLoader.Result result = assertLoadContact(Uri.EMPTY);
+        assertEquals(ContactDetailLoader.Result.ERROR, result);
+    }
+
+    public void testInvalidUri() {
+        ContactDetailLoader.Result result = assertLoadContact(Uri.parse("content://wtf"));
+        assertEquals(ContactDetailLoader.Result.ERROR, result);
+    }
+
+    public void testLoadContactWithContactIdUri() {
+        // Use content Uris that only contain the ID
+        final long contactId = 1;
+        final long rawContactId = 11;
+        final long dataId = 21;
+
+        final String encodedLookup = Uri.encode("aa%12%@!");
+        final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+        final Uri lookupUri = ContentUris.withAppendedId(
+                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, encodedLookup),
+                contactId);
+        final Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY);
+
+        ContactQueries queries = new ContactQueries();
+        queries.fetchLookupAndId(baseUri, contactId, encodedLookup);
+        queries.fetchHeaderData(baseUri, rawContactId, encodedLookup);
+        queries.fetchSocial(dataUri, contactId);
+        queries.fetchRawContacts(contactId, dataId, rawContactId);
+
+        ContactDetailLoader.Result contact = assertLoadContact(baseUri);
+
+        assertEquals(contactId, contact.getId());
+        assertEquals(rawContactId, contact.getNameRawContactId());
+        assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+        assertEquals(encodedLookup, contact.getLookupKey());
+        assertEquals(lookupUri, contact.getLookupUri());
+        assertEquals(1, contact.getEntities().size());
+        assertEquals(1, contact.getStatuses().size());
+        mContactsProvider.verify();
+    }
+
+    public void testLoadContactWithOldStyleUri() {
+        // Use content Uris that only contain the ID but use the format used in Donut
+        final long contactId = 1;
+        final long rawContactId = 11;
+        final long dataId = 21;
+
+        final String encodedLookup = Uri.encode("aa%12%@!");
+        final Uri legacyUri = ContentUris.withAppendedId(
+                Uri.parse("content://contacts"), rawContactId);
+        final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+        final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+        final Uri lookupUri = ContentUris.withAppendedId(
+                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, encodedLookup),
+                contactId);
+        final Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY);
+
+        ContactQueries queries = new ContactQueries();
+        queries.fetchContactIdAndLookupFromRawContactUri(rawContactUri, contactId, encodedLookup);
+        queries.fetchHeaderData(baseUri, rawContactId, encodedLookup);
+        queries.fetchSocial(dataUri, contactId);
+        queries.fetchRawContacts(contactId, dataId, rawContactId);
+
+        ContactDetailLoader.Result contact = assertLoadContact(legacyUri);
+
+        assertEquals(contactId, contact.getId());
+        assertEquals(rawContactId, contact.getNameRawContactId());
+        assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+        assertEquals(encodedLookup, contact.getLookupKey());
+        assertEquals(lookupUri, contact.getLookupUri());
+        assertEquals(1, contact.getEntities().size());
+        assertEquals(1, contact.getStatuses().size());
+        mContactsProvider.verify();
+    }
+
+    public void testLoadContactWithContactLookupUri() {
+        // Use lookup-style Uris that do not contain the Contact-ID
+
+        final long contactId = 1;
+        final long rawContactId = 11;
+        final long dataId = 21;
+
+        final String encodedLookup = Uri.encode("aa%12%@!");
+        final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+        final Uri lookupNoIdUri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, encodedLookup);
+        final Uri lookupUri = ContentUris.withAppendedId(lookupNoIdUri, contactId);
+        final Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY);
+
+        ContactQueries queries = new ContactQueries();
+        queries.fetchLookupAndId(lookupNoIdUri, contactId, encodedLookup);
+        queries.fetchHeaderData(baseUri, rawContactId, encodedLookup);
+        queries.fetchSocial(dataUri, contactId);
+        queries.fetchRawContacts(contactId, dataId, rawContactId);
+
+        ContactDetailLoader.Result contact = assertLoadContact(lookupNoIdUri);
+
+        assertEquals(contactId, contact.getId());
+        assertEquals(rawContactId, contact.getNameRawContactId());
+        assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+        assertEquals(encodedLookup, contact.getLookupKey());
+        assertEquals(lookupUri, contact.getLookupUri());
+        assertEquals(1, contact.getEntities().size());
+        assertEquals(1, contact.getStatuses().size());
+        mContactsProvider.verify();
+    }
+
+    public void testLoadContactWithContactLookupAndIdUri() {
+        // Use lookup-style Uris that also contain the Contact-ID
+        final long contactId = 1;
+        final long rawContactId = 11;
+        final long dataId = 21;
+
+        final String encodedLookup = Uri.encode("aa%12%@!");
+        final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+        final Uri lookupUri = ContentUris.withAppendedId(
+                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, encodedLookup),
+                contactId);
+        final Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY);
+
+        ContactQueries queries = new ContactQueries();
+        queries.fetchHeaderData(baseUri, rawContactId, encodedLookup);
+        queries.fetchSocial(dataUri, contactId);
+        queries.fetchRawContacts(contactId, dataId, rawContactId);
+
+        ContactDetailLoader.Result contact = assertLoadContact(lookupUri);
+
+        assertEquals(contactId, contact.getId());
+        assertEquals(rawContactId, contact.getNameRawContactId());
+        assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+        assertEquals(encodedLookup, contact.getLookupKey());
+        assertEquals(lookupUri, contact.getLookupUri());
+        assertEquals(1, contact.getEntities().size());
+        assertEquals(1, contact.getStatuses().size());
+        mContactsProvider.verify();
+    }
+
+    public void testLoadContactWithContactLookupWithIncorrectIdUri() {
+        // Use lookup-style Uris that contain incorrect Contact-ID
+        // (we want to ensure that still the correct contact is chosen)
+        // In this test, the incorrect Id references another Contact
+
+        final long contactId = 1;
+        final long wrongContactId = 2;
+        final long rawContactId = 11;
+        final long wrongRawContactId = 12;
+        final long dataId = 21;
+
+        final String encodedLookup = Uri.encode("aa%12%@!");
+        final String wrongEncodedLookup = Uri.encode("ab%12%@!");
+        final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+        final Uri wrongBaseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, wrongContactId);
+        final Uri lookupUri = ContentUris.withAppendedId(
+                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, encodedLookup),
+                contactId);
+        final Uri lookupWithWrongIdUri = ContentUris.withAppendedId(
+                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, encodedLookup),
+                wrongContactId);
+        final Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY);
+
+        ContactQueries queries = new ContactQueries();
+        queries.fetchHeaderData(wrongBaseUri, wrongRawContactId, wrongEncodedLookup);
+        queries.fetchLookupAndId(lookupWithWrongIdUri, contactId, encodedLookup);
+        queries.fetchHeaderData(baseUri, rawContactId, encodedLookup);
+        queries.fetchSocial(dataUri, contactId);
+        queries.fetchRawContacts(contactId, dataId, rawContactId);
+
+        ContactDetailLoader.Result contact = assertLoadContact(lookupWithWrongIdUri);
+
+        assertEquals(contactId, contact.getId());
+        assertEquals(rawContactId, contact.getNameRawContactId());
+        assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+        assertEquals(encodedLookup, contact.getLookupKey());
+        assertEquals(lookupUri, contact.getLookupUri());
+        assertEquals(1, contact.getEntities().size());
+        assertEquals(1, contact.getStatuses().size());
+
+        mContactsProvider.verify();
+    }
+
+    public void testLoadContactWithContactLookupWithIncorrectIdUri2() {
+        // Use lookup-style Uris that contain incorrect Contact-ID
+        // (we want to ensure that still the correct contact is chosen)
+        // In this test, the incorrect Id references no contact
+
+        final long contactId = 1;
+        final long wrongContactId = 2;
+        final long rawContactId = 11;
+        final long wrongRawContactId = 12;
+        final long dataId = 21;
+
+        final String encodedLookup = Uri.encode("aa%12%@!");
+        final String wrongEncodedLookup = Uri.encode("ab%12%@!");
+        final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+        final Uri wrongBaseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, wrongContactId);
+        final Uri lookupUri = ContentUris.withAppendedId(
+                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, encodedLookup),
+                contactId);
+        final Uri lookupWithWrongIdUri = ContentUris.withAppendedId(
+                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, encodedLookup),
+                wrongContactId);
+        final Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY);
+
+        ContactQueries queries = new ContactQueries();
+        queries.fetchHeaderDataNoResult(wrongBaseUri);
+        queries.fetchLookupAndId(lookupWithWrongIdUri, contactId, encodedLookup);
+        queries.fetchHeaderData(baseUri, rawContactId, encodedLookup);
+        queries.fetchSocial(dataUri, contactId);
+        queries.fetchRawContacts(contactId, dataId, rawContactId);
+
+        ContactDetailLoader.Result contact = assertLoadContact(lookupWithWrongIdUri);
+
+        assertEquals(contactId, contact.getId());
+        assertEquals(rawContactId, contact.getNameRawContactId());
+        assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+        assertEquals(encodedLookup, contact.getLookupKey());
+        assertEquals(lookupUri, contact.getLookupUri());
+        assertEquals(1, contact.getEntities().size());
+        assertEquals(1, contact.getStatuses().size());
+
+        mContactsProvider.verify();
+    }
+
+    public void testLoadContactWithContactLookupWithIncorrectIdUri3() {
+        // Use lookup-style Uris that contain incorrect Contact-ID
+        // (we want to ensure that still the correct contact is chosen)
+        // In this test, the incorrect Id references no contact and the lookup
+        // key can also not be resolved
+
+        final long contactId = 1;
+        final long wrongContactId = 2;
+        final long rawContactId = 11;
+        final long wrongRawContactId = 12;
+        final long dataId = 21;
+
+        final String encodedLookup = Uri.encode("aa%12%@!");
+        final String wrongEncodedLookup = Uri.encode("ab%12%@!");
+        final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+        final Uri wrongBaseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, wrongContactId);
+        final Uri lookupUri = ContentUris.withAppendedId(
+                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, encodedLookup),
+                contactId);
+        final Uri lookupWithWrongIdUri = ContentUris.withAppendedId(
+                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, encodedLookup),
+                wrongContactId);
+        final Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY);
+
+        ContactQueries queries = new ContactQueries();
+        queries.fetchHeaderDataNoResult(wrongBaseUri);
+        queries.fetchLookupAndIdNoResult(lookupWithWrongIdUri);
+
+        ContactDetailLoader.Result contact = assertLoadContact(lookupWithWrongIdUri);
+
+        assertEquals(ContactDetailLoader.Result.NOT_FOUND, contact);
+
+        mContactsProvider.verify();
+    }
+
+    private class ContactQueries {
+        private void fetchRawContacts(final long contactId, final long dataId,
+                final long rawContactId) {
+            mContactsProvider.expectQuery(RawContactsEntity.CONTENT_URI)
+                .withDefaultProjection(new String[] {
+                        RawContacts._ID, RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_TYPE,
+                        RawContacts.DIRTY, RawContacts.VERSION, RawContacts.SOURCE_ID,
+                        RawContacts.SYNC1, RawContacts.SYNC2, RawContacts.SYNC3, RawContacts.SYNC4,
+                        RawContacts.DELETED, RawContacts.CONTACT_ID, RawContacts.STARRED,
+                        RawContacts.IS_RESTRICTED, RawContacts.NAME_VERIFIED,
+
+                        Entity.DATA_ID, Data.RES_PACKAGE, Data.MIMETYPE, Data.IS_PRIMARY,
+                        Data.IS_SUPER_PRIMARY, Data.DATA_VERSION,
+                        CommonDataKinds.GroupMembership.GROUP_SOURCE_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,
+                        Data.SYNC1, Data.SYNC2, Data.SYNC3, Data.SYNC4
+                })
+                .withSelection(
+                        RawContacts.CONTACT_ID + "=?",
+                        new String[] { String.valueOf(contactId) } )
+                .returnRow(
+                        rawContactId, "mockAccountName", "mockAccountType",
+                        0, 1, "aa%12%@!",
+                        "", "", "", "",
+                        0, contactId, 0,
+                        0, 1,
+
+                        dataId, "", StructuredName.CONTENT_ITEM_TYPE, 1,
+                        1, 1,
+                        "mockGroupId",
+                        "dat1", "dat2", "dat3", "dat4", "dat5",
+                        "dat6", "dat7", "dat8", "dat9", "dat10",
+                        "dat11", "dat12", "dat13", "dat14", null,
+                        "syn1", "syn2", "syn3", "syn4");
+        }
+
+        private void fetchSocial(final Uri dataUri, final long expectedContactId) {
+            mContactsProvider.expectQuery(dataUri)
+                    .withProjection(
+                            Contacts._ID, StatusUpdates.STATUS, StatusUpdates.STATUS_RES_PACKAGE,
+                            StatusUpdates.STATUS_ICON, StatusUpdates.STATUS_LABEL,
+                            StatusUpdates.STATUS_TIMESTAMP, StatusUpdates.PRESENCE)
+                    .withSelection(
+                            StatusUpdates.PRESENCE +" IS NOT NULL OR " +
+                            StatusUpdates.STATUS + " IS NOT NULL",
+                            (String[]) null)
+                    .returnRow(
+                            expectedContactId, "This is a mock Status update", 0,
+                            1, 2,
+                            0, StatusUpdates.AVAILABLE);
+        }
+
+        private void fetchHeaderData(final Uri uri, final long expectedRawContactId,
+                final String expectedEncodedLookup) {
+            mContactsProvider.expectQuery(uri)
+                    .withProjection(
+                            Contacts.NAME_RAW_CONTACT_ID,
+                            Contacts.DISPLAY_NAME_SOURCE,
+                            Contacts.LOOKUP_KEY,
+                            Contacts.DISPLAY_NAME,
+                            Contacts.PHONETIC_NAME,
+                            Contacts.PHOTO_ID,
+                            Contacts.STARRED,
+                            Contacts.CONTACT_PRESENCE,
+                            Contacts.CONTACT_STATUS,
+                            Contacts.CONTACT_STATUS_TIMESTAMP,
+                            Contacts.CONTACT_STATUS_RES_PACKAGE,
+                            Contacts.CONTACT_STATUS_LABEL)
+                    .returnRow(
+                            expectedRawContactId,
+                            DisplayNameSources.STRUCTURED_NAME,
+                            expectedEncodedLookup,
+                            "contactDisplayName",
+                            "contactPhoneticName",
+                            null,
+                            0,
+                            null,
+                            null,
+                            null,
+                            null,
+                            null);
+        }
+
+        private void fetchHeaderDataNoResult(final Uri uri) {
+            mContactsProvider.expectQuery(uri)
+                    .withProjection(
+                            Contacts.NAME_RAW_CONTACT_ID,
+                            Contacts.DISPLAY_NAME_SOURCE,
+                            Contacts.LOOKUP_KEY,
+                            Contacts.DISPLAY_NAME,
+                            Contacts.PHONETIC_NAME,
+                            Contacts.PHOTO_ID,
+                            Contacts.STARRED,
+                            Contacts.CONTACT_PRESENCE,
+                            Contacts.CONTACT_STATUS,
+                            Contacts.CONTACT_STATUS_TIMESTAMP,
+                            Contacts.CONTACT_STATUS_RES_PACKAGE,
+                            Contacts.CONTACT_STATUS_LABEL);
+        }
+
+        private void fetchLookupAndId(final Uri sourceUri, final long expectedContactId,
+                final String expectedEncodedLookup) {
+            mContactsProvider.expectQuery(sourceUri)
+                    .withProjection(Contacts.LOOKUP_KEY, Contacts._ID)
+                    .returnRow(expectedEncodedLookup, expectedContactId);
+        }
+
+        private void fetchLookupAndIdNoResult(final Uri sourceUri) {
+            mContactsProvider.expectQuery(sourceUri)
+                    .withProjection(Contacts.LOOKUP_KEY, Contacts._ID);
+        }
+
+        private void fetchContactIdAndLookupFromRawContactUri(final Uri rawContactUri,
+                final long expectedContactId, final String expectedEncodedLookup) {
+            // TODO: use a lighter query by joining rawcontacts with contacts in provider
+            // (See ContactContracts.java)
+            final Uri dataUri = Uri.withAppendedPath(rawContactUri, Data.CONTENT_DIRECTORY);
+            mContactsProvider.expectQuery(dataUri)
+                    .withProjection(RawContacts.CONTACT_ID, Contacts.LOOKUP_KEY)
+                    .returnRow(expectedContactId, expectedEncodedLookup);
+        }
+    }
+}
diff --git a/tests/src/com/android/contacts/ContactDetailTest.java b/tests/src/com/android/contacts/ContactDetailTest.java
new file mode 100644
index 0000000..6e93bd9
--- /dev/null
+++ b/tests/src/com/android/contacts/ContactDetailTest.java
@@ -0,0 +1,45 @@
+package com.android.contacts;
+
+import com.android.contacts.activities.ContactDetailActivity;
+import com.android.contacts.tests.mocks.ContactsMockContext;
+import com.android.contacts.tests.mocks.MockContentProvider;
+import com.android.contacts.views.detail.ContactDetailLoader;
+
+import android.content.ContentUris;
+import android.content.Intent;
+import android.provider.ContactsContract.Contacts;
+import android.test.ActivityUnitTestCase;
+
+public class ContactDetailTest extends ActivityUnitTestCase<ContactDetailActivity> {
+    private ContactsMockContext mContext;
+    private MockContentProvider mContactsProvider;
+
+    public ContactDetailTest() {
+        super(ContactDetailActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mContext = new ContactsMockContext(getInstrumentation().getTargetContext());
+        mContactsProvider = mContext.getContactsProvider();
+        setActivityContext(mContext);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+//    public void testFoo() {
+//        // Use lookup-style Uris that also contain the Contact-ID
+//        //long rawContactId1 = mCreator.createRawContact("JohnDoe", "John", "Doe");
+//        //long contactId1 = mCreator.getContactIdByRawContactId(rawContactId1);
+//        //Uri contactUri1 = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId1);
+//        Intent intent = new Intent(Intent.ACTION_VIEW,
+//                ContentUris.withAppendedId(Contacts.CONTENT_URI, 123));
+//        startActivity(intent, null, null);
+//        ContactDetailActivity activity = getActivity();
+//        mContactsProvider.verify();
+//    }
+}
diff --git a/tests/src/com/android/contacts/ContactListModeTest.java b/tests/src/com/android/contacts/ContactListModeTest.java
new file mode 100644
index 0000000..f09cc8d
--- /dev/null
+++ b/tests/src/com/android/contacts/ContactListModeTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts;
+
+import com.android.contacts.tests.mocks.ContactsMockContext;
+import com.android.contacts.tests.mocks.MockContentProvider;
+
+import android.content.Intent;
+import android.provider.ContactsContract;
+import android.provider.Settings;
+import android.provider.ContactsContract.ContactCounts;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.ProviderStatus;
+import android.provider.ContactsContract.StatusUpdates;
+import android.test.ActivityUnitTestCase;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+
+/**
+ * Tests for the contact list activity modes.
+ *
+ * Running all tests:
+ *
+ *   runtest contacts
+ * or
+ *   adb shell am instrument \
+ *     -w com.android.contacts.tests/android.test.InstrumentationTestRunner
+ */
+public class ContactListModeTest
+        extends ActivityUnitTestCase<ContactsListActivity> {
+
+    private ContactsMockContext mContext;
+    private MockContentProvider mContactsProvider;
+    private MockContentProvider mSettingsProvider;
+
+    public ContactListModeTest() {
+        super(ContactsListActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mContext = new ContactsMockContext(getInstrumentation().getTargetContext());
+        mContactsProvider = mContext.getContactsProvider();
+        mSettingsProvider = mContext.getSettingsProvider();
+        setActivityContext(mContext);
+    }
+
+    public void testDefaultMode() throws Exception {
+        mContactsProvider.expectQuery(ProviderStatus.CONTENT_URI)
+                .withProjection(ProviderStatus.STATUS, ProviderStatus.DATA1);
+
+        mSettingsProvider.expectQuery(Settings.System.CONTENT_URI)
+                .withProjection(Settings.System.VALUE)
+                .withSelection(Settings.System.NAME + "=?",
+                        ContactsContract.Preferences.SORT_ORDER);
+
+        mSettingsProvider.expectQuery(Settings.System.CONTENT_URI)
+                .withProjection(Settings.System.VALUE)
+                .withSelection(Settings.System.NAME + "=?",
+                        ContactsContract.Preferences.DISPLAY_ORDER);
+
+        mContactsProvider.expectQuery(
+                Contacts.CONTENT_URI.buildUpon()
+                        .appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true")
+                        .build())
+                .withProjection(
+                        Contacts._ID,
+                        Contacts.DISPLAY_NAME,
+                        Contacts.DISPLAY_NAME_ALTERNATIVE,
+                        Contacts.SORT_KEY_PRIMARY,
+                        Contacts.STARRED,
+                        Contacts.TIMES_CONTACTED,
+                        Contacts.CONTACT_PRESENCE,
+                        Contacts.PHOTO_ID,
+                        Contacts.LOOKUP_KEY,
+                        Contacts.PHONETIC_NAME,
+                        Contacts.HAS_PHONE_NUMBER)
+                .withSelection(Contacts.IN_VISIBLE_GROUP + "=1")
+                .withSortOrder(Contacts.SORT_KEY_PRIMARY)
+                .returnRow(1, "John", "John", "john", 1, 10,
+                        StatusUpdates.AVAILABLE, 23, "lk1", "john", 1)
+                .returnRow(2, "Jim", "Jim", "jim", 1, 8,
+                        StatusUpdates.AWAY, 24, "lk2", "jim", 0);
+
+        Intent intent = new Intent(Intent.ACTION_VIEW, ContactsContract.Contacts.CONTENT_URI);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        startActivity(intent, null, null);
+        ContactsListActivity activity = getActivity();
+        activity.runQueriesSynchronously();
+
+        ListView listView = (ListView)activity.findViewById(android.R.id.list);
+        ListAdapter adapter = listView.getAdapter();
+        assertEquals(3, adapter.getCount());
+    }
+}
diff --git a/tests/src/com/android/contacts/tests/allintents/AllIntentsActivity.java b/tests/src/com/android/contacts/tests/allintents/AllIntentsActivity.java
new file mode 100644
index 0000000..b2c3072
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/allintents/AllIntentsActivity.java
@@ -0,0 +1,521 @@
+/*
+ * 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.tests.allintents;
+
+import com.android.contacts.ContactsSearchManager;
+import com.android.contacts.tests.R;
+
+import android.app.ListActivity;
+import android.app.SearchManager;
+import android.content.ComponentName;
+import android.content.ContentUris;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.Contacts.ContactMethods;
+import android.provider.Contacts.People;
+import android.provider.Contacts.Phones;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Intents;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.Intents.Insert;
+import android.provider.ContactsContract.Intents.UI;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.Toast;
+
+/**
+ * An activity that provides access to various modes of the contacts application.
+ * Useful for manual and scripted tests.
+ */
+@SuppressWarnings("deprecation")
+public class AllIntentsActivity extends ListActivity {
+
+    private static final String ANDROID_CONTACTS_PACKAGE = "com.android.contacts";
+
+    private static final String CONTACTS_LIST_ACTIVITY_CLASS_NAME =
+            "com.android.contacts.ContactsListActivity";
+    private static final String SEARCH_RESULTS_ACTIVITY_CLASS_NAME =
+            "com.android.contacts.SearchResultsActivity";
+    private static final String MULTIPLE_PHONE_PICKER_ACTIVITY_CLASS_NAME =
+        "com.android.contacts.MultiplePhonePickerActivity";
+
+    private static final int LIST_DEFAULT = 0;
+    private static final int LIST_ALL_CONTACTS_ACTION = 1;
+    private static final int LIST_CONTACTS_WITH_PHONES_ACTION = 2;
+    private static final int LIST_STARRED_ACTION = 3;
+    private static final int LIST_STARRED_ACTION_WITH_FILTER = 4;
+    private static final int LIST_FREQUENT_ACTION = 5;
+    private static final int LIST_FREQUENT_ACTION_WITH_FILTER = 6;
+    private static final int LIST_STREQUENT_ACTION = 7;
+    private static final int LIST_STREQUENT_ACTION_WITH_FILTER = 8;
+    private static final int ACTION_PICK_CONTACT = 9;
+    private static final int ACTION_PICK_CONTACT_LEGACY = 10;
+    private static final int ACTION_PICK_PHONE = 11;
+    private static final int ACTION_PICK_PHONE_LEGACY = 12;
+    private static final int ACTION_PICK_POSTAL = 13;
+    private static final int ACTION_PICK_POSTAL_LEGACY = 14;
+    private static final int ACTION_CREATE_SHORTCUT_CONTACT = 15;
+    private static final int ACTION_CREATE_SHORTCUT_CONTACT_FILTER = 16;
+    private static final int ACTION_CREATE_SHORTCUT_DIAL = 17;
+    private static final int ACTION_CREATE_SHORTCUT_DIAL_FILTER = 18;
+    private static final int ACTION_CREATE_SHORTCUT_MESSAGE = 19;
+    private static final int ACTION_CREATE_SHORTCUT_MESSAGE_FILTER = 20;
+    private static final int ACTION_GET_CONTENT_CONTACT = 21;
+    private static final int ACTION_GET_CONTENT_CONTACT_FILTER = 22;
+    private static final int ACTION_GET_CONTENT_CONTACT_LEGACY = 23;
+    private static final int ACTION_GET_CONTENT_CONTACT_FILTER_LEGACY = 24;
+    private static final int ACTION_GET_CONTENT_PHONE = 25;
+    private static final int ACTION_GET_CONTENT_PHONE_FILTER = 26;
+    private static final int ACTION_GET_CONTENT_PHONE_LEGACY = 27;
+    private static final int ACTION_GET_CONTENT_POSTAL = 28;
+    private static final int ACTION_GET_CONTENT_POSTAL_FILTER = 29;
+    private static final int ACTION_GET_CONTENT_POSTAL_LEGACY = 30;
+    private static final int ACTION_INSERT_OR_EDIT = 31;
+    private static final int ACTION_SEARCH_CALL = 32;
+    private static final int ACTION_SEARCH_CONTACT = 33;
+    private static final int ACTION_SEARCH_EMAIL = 34;
+    private static final int ACTION_SEARCH_PHONE = 35;
+    private static final int SEARCH_SUGGESTION_CLICKED_CALL_BUTTON = 36;
+    private static final int SEARCH_SUGGESTION_CLICKED_CONTACT = 37;
+    private static final int SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED = 38;
+    private static final int SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED = 39;
+    private static final int JOIN_CONTACT = 40;
+    private static final int ACTION_GET_MULTIPLE_PHONES = 41;
+
+    private static final int EDIT_CONTACT = 42;
+    private static final int EDIT_CONTACT_LOOKUP = 43;
+    private static final int EDIT_CONTACT_LOOKUP_ID = 44;
+    private static final int EDIT_RAW_CONTACT = 45;
+    private static final int EDIT_LEGACY = 46;
+    private static final int EDIT_NEW_CONTACT = 47;
+    private static final int EDIT_NEW_RAW_CONTACT = 48;
+    private static final int EDIT_NEW_LEGACY = 49;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setListAdapter(new ArrayAdapter<String>(this, R.layout.intent_list_item,
+                getResources().getStringArray(R.array.allIntents)));
+    }
+
+    @Override
+    protected void onListItemClick(ListView l, View v, int position, long id) {
+        super.onListItemClick(l, v, position, id);
+
+        switch (position) {
+            case LIST_DEFAULT: {
+                startContactsListActivity(
+                        new Intent(Intent.ACTION_VIEW, Contacts.CONTENT_URI));
+                break;
+            }
+            case LIST_ALL_CONTACTS_ACTION: {
+                startContactsListActivity(
+                        new Intent(UI.LIST_ALL_CONTACTS_ACTION, Contacts.CONTENT_URI));
+                break;
+            }
+            case LIST_CONTACTS_WITH_PHONES_ACTION: {
+                startContactsListActivity(
+                        new Intent(UI.LIST_CONTACTS_WITH_PHONES_ACTION, Contacts.CONTENT_URI));
+                break;
+            }
+            case LIST_STARRED_ACTION: {
+                startContactsListActivity(
+                        new Intent(UI.LIST_STARRED_ACTION, Contacts.CONTENT_URI));
+                break;
+            }
+            case LIST_STARRED_ACTION_WITH_FILTER: {
+                startContactsListActivity(
+                        buildFilterIntent(UI.LIST_STARRED_ACTION, null, null));
+                break;
+            }
+            case LIST_FREQUENT_ACTION: {
+                startContactsListActivity(
+                        new Intent(UI.LIST_FREQUENT_ACTION, Contacts.CONTENT_URI));
+                break;
+            }
+            case LIST_FREQUENT_ACTION_WITH_FILTER: {
+                startContactsListActivity(
+                        buildFilterIntent(UI.LIST_FREQUENT_ACTION, null, null));
+                break;
+            }
+            case LIST_STREQUENT_ACTION: {
+                startContactsListActivity(
+                        new Intent(UI.LIST_STREQUENT_ACTION, Contacts.CONTENT_URI));
+                break;
+            }
+            case LIST_STREQUENT_ACTION_WITH_FILTER: {
+                startContactsListActivity(
+                        buildFilterIntent(UI.LIST_STREQUENT_ACTION, null, null));
+                break;
+            }
+            case ACTION_PICK_CONTACT: {
+                startContactsListActivityForResult(
+                        new Intent(Intent.ACTION_PICK, Contacts.CONTENT_URI));
+                break;
+            }
+            case ACTION_PICK_CONTACT_LEGACY: {
+                startContactsListActivityForResult(
+                        new Intent(Intent.ACTION_PICK, People.CONTENT_URI));
+                break;
+            }
+            case ACTION_PICK_PHONE: {
+                startContactsListActivityForResult(
+                        new Intent(Intent.ACTION_PICK, Phone.CONTENT_URI));
+                break;
+            }
+            case ACTION_PICK_PHONE_LEGACY: {
+                startContactsListActivityForResult(
+                        new Intent(Intent.ACTION_PICK, Phones.CONTENT_URI));
+                break;
+            }
+            case ACTION_PICK_POSTAL: {
+                startContactsListActivityForResult(
+                        new Intent(Intent.ACTION_PICK, StructuredPostal.CONTENT_URI));
+                break;
+            }
+            case ACTION_PICK_POSTAL_LEGACY: {
+                Intent intent = new Intent(Intent.ACTION_PICK);
+                intent.setType(ContactMethods.CONTENT_POSTAL_TYPE);
+                startContactsListActivityForResult(intent);
+                break;
+            }
+            case ACTION_CREATE_SHORTCUT_CONTACT: {
+                Intent intent = new Intent(Intent.ACTION_CREATE_SHORTCUT);
+                startContactsListActivityForResult(intent);
+                break;
+            }
+            case ACTION_CREATE_SHORTCUT_CONTACT_FILTER: {
+                startContactsListActivityForResult(
+                        buildFilterIntent(Intent.ACTION_CREATE_SHORTCUT,
+                                CONTACTS_LIST_ACTIVITY_CLASS_NAME, null));
+                break;
+            }
+            case ACTION_CREATE_SHORTCUT_DIAL: {
+                Intent intent = new Intent(Intent.ACTION_CREATE_SHORTCUT);
+                intent.setComponent(
+                        new ComponentName(ANDROID_CONTACTS_PACKAGE, "alias.DialShortcut"));
+                startActivityForResult(intent, 0);
+                break;
+            }
+            case ACTION_CREATE_SHORTCUT_DIAL_FILTER: {
+                startContactsListActivityForResult(
+                        buildFilterIntent(Intent.ACTION_CREATE_SHORTCUT,
+                                "alias.DialShortcut", null));
+                break;
+            }
+            case ACTION_CREATE_SHORTCUT_MESSAGE: {
+                Intent intent = new Intent(Intent.ACTION_CREATE_SHORTCUT);
+                intent.setComponent(
+                        new ComponentName(ANDROID_CONTACTS_PACKAGE, "alias.MessageShortcut"));
+                startActivityForResult(intent, 0);
+                break;
+            }
+            case ACTION_CREATE_SHORTCUT_MESSAGE_FILTER: {
+                startContactsListActivityForResult(
+                        buildFilterIntent(Intent.ACTION_CREATE_SHORTCUT,
+                                "alias.MessageShortcut", null));
+                break;
+            }
+            case ACTION_GET_CONTENT_CONTACT: {
+                Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+                intent.setType(Contacts.CONTENT_ITEM_TYPE);
+                startContactsListActivityForResult(intent);
+                break;
+            }
+            case ACTION_GET_CONTENT_CONTACT_LEGACY: {
+                Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+                intent.setType(People.CONTENT_ITEM_TYPE);
+                startContactsListActivityForResult(intent);
+                break;
+            }
+            case ACTION_GET_CONTENT_CONTACT_FILTER: {
+                startContactsListActivityForResult(
+                        buildFilterIntent(Intent.ACTION_GET_CONTENT,
+                                CONTACTS_LIST_ACTIVITY_CLASS_NAME,
+                                Contacts.CONTENT_ITEM_TYPE));
+                break;
+            }
+            case ACTION_GET_CONTENT_CONTACT_FILTER_LEGACY: {
+                startContactsListActivityForResult(
+                        buildFilterIntent(Intent.ACTION_GET_CONTENT,
+                                CONTACTS_LIST_ACTIVITY_CLASS_NAME,
+                                People.CONTENT_ITEM_TYPE));
+                break;
+            }
+            case ACTION_GET_CONTENT_PHONE: {
+                Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+                intent.setType(Phone.CONTENT_ITEM_TYPE);
+                startContactsListActivityForResult(intent);
+                break;
+            }
+            case ACTION_GET_CONTENT_PHONE_FILTER: {
+                startContactsListActivityForResult(
+                        buildFilterIntent(Intent.ACTION_GET_CONTENT,
+                                CONTACTS_LIST_ACTIVITY_CLASS_NAME,
+                                Phone.CONTENT_ITEM_TYPE));
+                break;
+            }
+            case ACTION_GET_CONTENT_PHONE_LEGACY: {
+                Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+                intent.setType(Phones.CONTENT_ITEM_TYPE);
+                startContactsListActivityForResult(intent);
+                break;
+            }
+            case ACTION_GET_CONTENT_POSTAL: {
+                Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+                intent.setType(StructuredPostal.CONTENT_ITEM_TYPE);
+                startContactsListActivityForResult(intent);
+                break;
+            }
+            case ACTION_GET_CONTENT_POSTAL_FILTER: {
+                startContactsListActivityForResult(
+                        buildFilterIntent(Intent.ACTION_GET_CONTENT,
+                                CONTACTS_LIST_ACTIVITY_CLASS_NAME,
+                                StructuredPostal.CONTENT_ITEM_TYPE));
+                break;
+            }
+            case ACTION_GET_CONTENT_POSTAL_LEGACY: {
+                Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+                intent.setType(ContactMethods.CONTENT_POSTAL_ITEM_TYPE);
+                startContactsListActivityForResult(intent);
+                break;
+            }
+            case ACTION_INSERT_OR_EDIT: {
+                Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
+                startContactsListActivity(intent);
+                break;
+            }
+            case ACTION_SEARCH_CALL: {
+                Intent intent = new Intent(Intent.ACTION_SEARCH);
+                intent.putExtra(SearchManager.ACTION_MSG, "call");
+                intent.putExtra(SearchManager.QUERY, "800-4664-411");
+                startSearchResultActivity(intent);
+                break;
+            }
+            case ACTION_SEARCH_CONTACT: {
+                Intent intent = new Intent(Intent.ACTION_SEARCH);
+                intent.putExtra(SearchManager.QUERY, "a");
+                startSearchResultActivity(intent);
+                break;
+            }
+            case ACTION_SEARCH_EMAIL: {
+                Intent intent = new Intent(Intent.ACTION_SEARCH);
+                intent.putExtra(Insert.EMAIL, "a");
+                startSearchResultActivity(intent);
+                break;
+            }
+            case ACTION_SEARCH_PHONE: {
+                Intent intent = new Intent(Intent.ACTION_SEARCH);
+                intent.putExtra(Insert.PHONE, "800");
+                startSearchResultActivity(intent);
+                break;
+            }
+            case SEARCH_SUGGESTION_CLICKED_CALL_BUTTON: {
+                long contactId = findArbitraryContactWithPhoneNumber();
+                if (contactId != -1) {
+                    Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+                    Intent intent = new Intent(Intents.SEARCH_SUGGESTION_CLICKED);
+                    intent.setData(contactUri);
+                    intent.putExtra(SearchManager.ACTION_MSG, "call");
+                    startContactsListActivity(intent);
+                }
+                break;
+            }
+            case SEARCH_SUGGESTION_CLICKED_CONTACT: {
+                long contactId = findArbitraryContactWithPhoneNumber();
+                if (contactId != -1) {
+                    Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+                    Intent intent = new Intent(Intents.SEARCH_SUGGESTION_CLICKED);
+                    intent.setData(contactUri);
+                    startContactsListActivity(intent);
+                }
+                break;
+            }
+            case SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED: {
+                Intent intent = new Intent(Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED);
+                intent.setData(Uri.parse("tel:800-4664411"));
+                startContactsListActivity(intent);
+                break;
+            }
+            case SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED: {
+                Intent intent = new Intent(Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED);
+                intent.setData(Uri.parse("tel:800-4664411"));
+                startContactsListActivity(intent);
+                break;
+            }
+            case JOIN_CONTACT: {
+                // TODO
+                break;
+            }
+            case ACTION_GET_MULTIPLE_PHONES: {
+                Intent intent = new Intent(Intents.ACTION_GET_MULTIPLE_PHONES);
+                intent.setType(Phone.CONTENT_TYPE);
+                intent.putExtra(Intents.EXTRA_PHONE_URIS, new Uri[] {
+                        Uri.parse("tel:555-1212"), Uri.parse("tel:555-2121")
+                });
+                startMultiplePhoneSelectionActivityForResult(intent);
+                break;
+            }
+            case EDIT_CONTACT: {
+                final long contactId = findArbitraryContactWithPhoneNumber();
+                final Uri uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+                final Intent intent = new Intent(Intent.ACTION_EDIT, uri);
+                startActivity(intent);
+                break;
+            }
+            case EDIT_CONTACT_LOOKUP: {
+                final long contactId = findArbitraryContactWithPhoneNumber();
+                final Uri uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+                final Uri lookupUri = Contacts.getLookupUri(getContentResolver(), uri);
+                final String lookupKey = lookupUri.getPathSegments().get(2);
+                final Uri lookupWithoutIdUri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI,
+                        lookupKey);
+                final Intent intent = new Intent(Intent.ACTION_EDIT, lookupWithoutIdUri);
+                startActivity(intent);
+                break;
+            }
+            case EDIT_CONTACT_LOOKUP_ID: {
+                final long contactId = findArbitraryContactWithPhoneNumber();
+                final Uri uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+                final Uri lookupUri = Contacts.getLookupUri(getContentResolver(), uri);
+                final Intent intent = new Intent(Intent.ACTION_EDIT, lookupUri);
+                startActivity(intent);
+                break;
+            }
+            case EDIT_RAW_CONTACT: {
+                final long contactId = findArbitraryContactWithPhoneNumber();
+                final long rawContactId = findArbitraryRawContactOfContact(contactId);
+                final Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+                final Intent intent = new Intent(Intent.ACTION_EDIT, uri);
+                startActivity(intent);
+                break;
+            }
+            case EDIT_LEGACY: {
+                final Uri legacyContentUri = Uri.parse("content://contacts/people");
+                final long contactId = findArbitraryContactWithPhoneNumber();
+                final long rawContactId = findArbitraryRawContactOfContact(contactId);
+                final Uri uri = ContentUris.withAppendedId(legacyContentUri, rawContactId);
+                final Intent intent = new Intent(Intent.ACTION_EDIT, uri);
+                startActivity(intent);
+                break;
+            }
+            case EDIT_NEW_CONTACT: {
+                startActivity(new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI));
+                break;
+            }
+            case EDIT_NEW_RAW_CONTACT: {
+                startActivity(new Intent(Intent.ACTION_INSERT, RawContacts.CONTENT_URI));
+                break;
+            }
+            case EDIT_NEW_LEGACY: {
+                final Uri legacyContentUri = Uri.parse("content://contacts/people");
+                startActivity(new Intent(Intent.ACTION_INSERT, legacyContentUri));
+                break;
+            }
+            default: {
+                Toast.makeText(this, "Sorry, we forgot to write this...", Toast.LENGTH_LONG).show();
+            }
+        }
+    }
+
+    private Intent buildFilterIntent(String action, String component, String type) {
+        Intent intent = new Intent(UI.FILTER_CONTACTS_ACTION);
+        intent.putExtra(UI.FILTER_TEXT_EXTRA_KEY, "A");
+        intent.putExtra(ContactsSearchManager.ORIGINAL_ACTION_EXTRA_KEY, action);
+        if (component != null) {
+            intent.putExtra(ContactsSearchManager.ORIGINAL_COMPONENT_EXTRA_KEY, component);
+        }
+        if (type != null) {
+            intent.putExtra(ContactsSearchManager.ORIGINAL_TYPE_EXTRA_KEY, type);
+        }
+        return intent;
+    }
+
+    private void startContactsListActivity(Intent intent) {
+        intent.setComponent(
+                new ComponentName(ANDROID_CONTACTS_PACKAGE, CONTACTS_LIST_ACTIVITY_CLASS_NAME));
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        startActivity(intent);
+    }
+
+    private void startContactsListActivityForResult(Intent intent) {
+        intent.setComponent(
+                new ComponentName(ANDROID_CONTACTS_PACKAGE, CONTACTS_LIST_ACTIVITY_CLASS_NAME));
+        startActivityForResult(intent, 12);
+    }
+
+    private void startSearchResultActivity(Intent intent) {
+        intent.setComponent(
+                new ComponentName(ANDROID_CONTACTS_PACKAGE, SEARCH_RESULTS_ACTIVITY_CLASS_NAME));
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        startActivity(intent);
+    }
+
+    private void startMultiplePhoneSelectionActivityForResult(Intent intent) {
+        intent.setComponent(
+                new ComponentName(ANDROID_CONTACTS_PACKAGE,
+                        MULTIPLE_PHONE_PICKER_ACTIVITY_CLASS_NAME));
+        startActivityForResult(intent, 13);
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        Intent intent = new Intent(this, ResultActivity.class);
+        intent.putExtra("resultCode", resultCode);
+        intent.putExtra("data", data);
+        startActivity(intent);
+    }
+
+    private long findArbitraryContactWithPhoneNumber() {
+        final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI,
+                new String[] { Contacts._ID },
+                Contacts.HAS_PHONE_NUMBER + "!=0 AND " + Contacts.STARRED + "!=0" ,
+                null, "RANDOM() LIMIT 1");
+        try {
+            if (cursor.moveToFirst()) {
+                return cursor.getLong(0);
+            }
+        } finally {
+            cursor.close();
+        }
+
+        return -1;
+    }
+
+    private long findArbitraryRawContactOfContact(long contactId) {
+        final Cursor cursor = getContentResolver().query(RawContacts.CONTENT_URI,
+                new String[] { RawContacts._ID },
+                RawContacts.CONTACT_ID + "=?",
+                new String[] { String.valueOf(contactId) },
+                RawContacts._ID + " LIMIT 1");
+        try {
+            if (cursor.moveToFirst()) {
+                return cursor.getLong(0);
+            }
+        } finally {
+            cursor.close();
+        }
+
+        return -1;
+    }
+}
diff --git a/tests/src/com/android/contacts/tests/allintents/ResultActivity.java b/tests/src/com/android/contacts/tests/allintents/ResultActivity.java
new file mode 100644
index 0000000..562f2ba
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/allintents/ResultActivity.java
@@ -0,0 +1,196 @@
+/*
+ * 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.tests.allintents;
+
+import com.android.contacts.tests.R;
+
+import android.app.Activity;
+import android.content.ContentUris;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TableLayout;
+import android.widget.TableRow;
+import android.widget.TextView;
+import android.widget.ImageView.ScaleType;
+
+import java.util.Arrays;
+
+/**
+ * An activity that shows the result of a contacts activity invocation.
+ */
+public class ResultActivity extends Activity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.result);
+
+        Intent intent = getIntent();
+        addRowsForIntent((Intent)intent.getExtras().get("data"));
+    }
+
+    private void addRowsForIntent(Intent intent) {
+        if (intent == null) {
+            addRow("", "No data intent returned");
+        } else {
+            addRow("INTENT", intent.toString());
+            addSeparator(3);
+
+            Bundle extras = intent.getExtras();
+            if (extras != null && !extras.isEmpty()) {
+                for (String key : extras.keySet()) {
+                    Object value = extras.get(key);
+                    addRow("EXTRA", key);
+                    addRowForValue("", value);
+                }
+
+                addSeparator(3);
+            }
+
+            String dataUri = intent.getDataString();
+            if (dataUri != null) {
+                addRowsForQuery(Uri.parse(dataUri));
+            }
+        }
+    }
+
+    private void addRowForValue(String label, Object value) {
+        if (value == null) {
+            addRow(label, "null");
+        } else if (value instanceof Bitmap) {
+            addRowWithBitmap(label, (Bitmap)value);
+        } else if (value instanceof Intent) {
+            addRow(label, "INTENT");
+            addRowsForIntent((Intent)value);
+        } else if (value instanceof Uri) {
+            addRow(label, "DATA");
+            addRowsForQuery((Uri)value);
+        } else if (value.getClass().isArray()) {
+            addRow(label, "ARRAY");
+            Parcelable[] array = (Parcelable[])value;
+            for (int i = 0; i < array.length; i++) {
+                addRowForValue("[" + i + "]", String.valueOf(array[i]));
+            }
+        } else {
+            addRow(label, String.valueOf(value));
+        }
+    }
+
+    private void addRowsForQuery(Uri dataUri) {
+        Cursor cursor = getContentResolver().query(dataUri, null, null, null, null);
+        if (cursor == null) {
+            addRow("", "No data for this URI");
+        } else {
+            try {
+                while (cursor.moveToNext()) {
+                    addRow("", "DATA");
+                    String[] columnNames = cursor.getColumnNames();
+                    String[] names = new String[columnNames.length];
+                    System.arraycopy(columnNames, 0, names, 0, columnNames.length);
+                    Arrays.sort(names);
+                    for (int i = 0; i < names.length; i++) {
+                        int index = cursor.getColumnIndex(names[i]);
+                        String value = cursor.getString(index);
+                        addRow(names[i], value);
+
+                        if (names[i].equals(Contacts.PHOTO_ID) && !TextUtils.isEmpty(value)) {
+                            addRowWithPhoto(Long.parseLong(value));
+                        }
+                    }
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+    }
+
+    private void addRow(String column0, String column1) {
+        TextView label = new TextView(this);
+        label.setPadding(4, 4, 4, 4);
+        label.setText(column0);
+        TextView value = new TextView(this);
+        value.setPadding(4, 4, 4, 4);
+        value.setText(column1);
+        addRow(label, value);
+    }
+
+    private void addRowWithPhoto(long photoId) {
+        byte[] data = null;
+        Cursor cursor = getContentResolver().query(
+                ContentUris.withAppendedId(Data.CONTENT_URI, photoId),
+                new String[]{Photo.PHOTO}, null, null, null);
+        try {
+            if (cursor.moveToNext()) {
+                data = cursor.getBlob(0);
+            }
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+
+        if (data == null) {
+            return;
+        }
+
+        addRowWithBitmap("Photo", BitmapFactory.decodeByteArray(data, 0, data.length));
+    }
+
+    private void addRowWithBitmap(String label, Bitmap bitmap) {
+        TextView labelView = new TextView(this);
+        labelView.setPadding(4, 4, 4, 4);
+        labelView.setText(label);
+
+        ImageView imageView = new ImageView(this);
+        imageView.setImageBitmap(bitmap);
+        imageView.setPadding(4, 4, 4, 4);
+        imageView.setScaleType(ScaleType.FIT_START);
+        addRow(labelView, imageView);
+    }
+
+    private void addRow(View column0, View column1) {
+        TableLayout table = (TableLayout)findViewById(R.id.table);
+        TableRow row = new TableRow(this);
+        row.addView(column0);
+        row.addView(column1);
+        table.addView(row);
+
+        addSeparator(1);
+    }
+
+    private void addSeparator(int height) {
+        TableLayout table = (TableLayout)findViewById(R.id.table);
+        View separator = new View(this);
+        TableLayout.LayoutParams params = new TableLayout.LayoutParams();
+        params.height = height;
+        separator.setLayoutParams(params);
+        separator.setBackgroundColor(Color.rgb(33, 66, 33));
+        table.addView(separator);
+    }
+}
diff --git a/tests/src/com/android/contacts/tests/mocks/ContactsMockContext.java b/tests/src/com/android/contacts/tests/mocks/ContactsMockContext.java
new file mode 100644
index 0000000..4697b83
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/mocks/ContactsMockContext.java
@@ -0,0 +1,63 @@
+/*
+ * 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.tests.mocks;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.provider.ContactsContract;
+import android.provider.Settings;
+import android.test.mock.MockContentResolver;
+
+/**
+ * A mock context for contact activity unit tests. Forwards everything to
+ * a supplied context, except content resolver operations, which are sent
+ * to mock content providers.
+ */
+public class ContactsMockContext extends ContextWrapper {
+
+    private MockContentResolver mContentResolver;
+    private MockContentProvider mContactsProvider;
+    private MockContentProvider mSettingsProvider;
+
+    public ContactsMockContext(Context base) {
+        super(base);
+        mContentResolver = new MockContentResolver();
+        mContactsProvider = new MockContentProvider();
+        mContentResolver.addProvider(ContactsContract.AUTHORITY, mContactsProvider);
+        mSettingsProvider = new MockContentProvider();
+        mContentResolver.addProvider(Settings.AUTHORITY, mSettingsProvider);
+    }
+
+    @Override
+    public ContentResolver getContentResolver() {
+        return mContentResolver;
+    }
+
+    public MockContentProvider getContactsProvider() {
+        return mContactsProvider;
+    }
+
+    public MockContentProvider getSettingsProvider() {
+        return mSettingsProvider;
+    }
+
+    @Override
+    public Context getApplicationContext() {
+        return this;
+    }
+}
diff --git a/tests/src/com/android/contacts/tests/mocks/MockContentProvider.java b/tests/src/com/android/contacts/tests/mocks/MockContentProvider.java
new file mode 100644
index 0000000..00e1f03
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/mocks/MockContentProvider.java
@@ -0,0 +1,214 @@
+/*
+ * 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.tests.mocks;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
+
+import junit.framework.Assert;
+
+/**
+ * A programmable mock content provider.
+ */
+public class MockContentProvider extends ContentProvider {
+
+    public static class Query {
+
+        private final Uri mUri;
+        private String[] mProjection;
+        private String[] mDefaultProjection;
+        private String mSelection;
+        private String[] mSelectionArgs;
+        private String mSortOrder;
+        private ArrayList<Object[]> mRows = new ArrayList<Object[]>();
+
+        public Query(Uri uri) {
+            mUri = uri;
+        }
+
+        @Override
+        public String toString() {
+            return queryToString(mUri, mProjection, mSelection, mSelectionArgs, mSortOrder);
+        }
+
+        public Query withProjection(String... projection) {
+            mProjection = projection;
+            return this;
+        }
+
+        public Query withDefaultProjection(String... projection) {
+            mDefaultProjection = projection;
+            return this;
+        }
+
+        public Query withSelection(String selection, String... selectionArgs) {
+            mSelection = selection;
+            mSelectionArgs = selectionArgs;
+            return this;
+        }
+
+        public Query withSortOrder(String sortOrder) {
+            mSortOrder = sortOrder;
+            return this;
+        }
+
+        public Query returnRow(Object... row) {
+            mRows.add(row);
+            return this;
+        }
+
+        public boolean equals(Uri uri, String[] projection, String selection,
+                String[] selectionArgs, String sortOrder) {
+            if (!uri.equals(mUri)) {
+                return false;
+            }
+
+            if (!equals(projection, mProjection)) {
+                return false;
+            }
+
+            if (!TextUtils.equals(selection, mSelection)) {
+                return false;
+            }
+
+            if (!equals(selectionArgs, mSelectionArgs)) {
+                return false;
+            }
+
+            if (!TextUtils.equals(sortOrder, mSortOrder)) {
+                return false;
+            }
+
+            return true;
+        }
+
+        private static boolean equals(String[] array1, String[] array2) {
+            boolean empty1 = array1 == null || array1.length == 0;
+            boolean empty2 = array2 == null || array2.length == 0;
+            if (empty1 && empty2) {
+                return true;
+            }
+            if (empty1) {
+                return false;
+            }
+
+            if (array1.length != array2.length) return false;
+
+            for (int i = 0; i < array1.length; i++) {
+                if (!array1[i].equals(array2[i])) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        public Cursor getResult() {
+            String[] columnNames = mProjection != null ? mProjection : mDefaultProjection;
+            MatrixCursor cursor = new MatrixCursor(columnNames);
+            for (Object[] row : mRows) {
+                cursor.addRow(row);
+            }
+            return cursor;
+        }
+    }
+
+    private LinkedList<Query> mExpectedQueries = new LinkedList<Query>();
+
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    public Query expectQuery(Uri contentUri) {
+        Query query = new Query(contentUri);
+        mExpectedQueries.offer(query);
+        return query;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        if (mExpectedQueries.isEmpty()) {
+            Assert.fail("Unexpected query: "
+                    + queryToString(uri, projection, selection, selectionArgs, sortOrder));
+        }
+
+        Query query = mExpectedQueries.remove();
+        if (!query.equals(uri, projection, selection, selectionArgs, sortOrder)) {
+            Assert.fail("Incorrect query.\n    Expected: " + query + "\n      Actual: " +
+                    queryToString(uri, projection, selection, selectionArgs, sortOrder));
+        }
+
+        return query.getResult();
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+
+    private static String queryToString(Uri uri, String[] projection, String selection,
+            String[] selectionArgs, String sortOrder) {
+        StringBuilder sb = new StringBuilder();
+        sb.append(uri).append(" ");
+        if (projection != null) {
+            sb.append(Arrays.toString(projection));
+        } else {
+            sb.append("[]");
+        }
+        if (selection != null) {
+            sb.append(" selection: '").append(selection).append("'");
+            if (selectionArgs != null) {
+                sb.append(Arrays.toString(selectionArgs));
+            } else {
+                sb.append("[]");
+            }
+        }
+        if (sortOrder != null) {
+            sb.append(" sort: '").append(sortOrder).append("'");
+        }
+        return sb.toString();
+    }
+
+    public void verify() {
+        Assert.assertTrue("Not all expected queries have been called: " +
+                mExpectedQueries, mExpectedQueries.isEmpty());
+    }
+}
diff --git a/tests/src/com/android/contacts/widget/CompositeListAdapterTest.java b/tests/src/com/android/contacts/widget/CompositeListAdapterTest.java
new file mode 100644
index 0000000..87d268b
--- /dev/null
+++ b/tests/src/com/android/contacts/widget/CompositeListAdapterTest.java
@@ -0,0 +1,324 @@
+/*
+ * 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.widget;
+
+import com.google.android.collect.Lists;
+
+import android.content.Context;
+import android.database.DataSetObserver;
+import android.test.AndroidTestCase;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Tests for {@link CompositeListAdapter}.
+ */
+public class CompositeListAdapterTest extends AndroidTestCase {
+
+    private final class MockAdapter extends ArrayAdapter<String> {
+        boolean allItemsEnabled = true;
+        HashSet<Integer> enabledItems = new HashSet<Integer>();
+        int viewTypeCount = 1;
+        HashMap<Integer, Integer> viewTypes = new HashMap<Integer, Integer>();
+
+        private MockAdapter(Context context, List<String> objects) {
+            super(context, android.R.layout.simple_list_item_1, objects);
+            for (int i = 0; i < objects.size(); i++) {
+                viewTypes.put(i, 0);
+            }
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            return new MockView(getContext(), position);
+        }
+
+        @Override
+        public boolean areAllItemsEnabled() {
+            return allItemsEnabled;
+        }
+
+        @Override
+        public boolean isEnabled(int position) {
+            return enabledItems.contains(position);
+        }
+
+        @Override
+        public int getViewTypeCount() {
+            return viewTypeCount;
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            return viewTypes.get(position);
+        }
+    }
+
+    private final class MockView extends View {
+        public MockView(Context context, int position) {
+            super(context);
+            setTag(position);
+        }
+    }
+
+    private final class TestDataSetObserver extends DataSetObserver {
+
+        public int changeCount;
+        public int invalidationCount;
+
+        @Override
+        public void onChanged() {
+            changeCount++;
+        }
+
+        @Override
+        public void onInvalidated() {
+            invalidationCount++;
+        }
+    }
+
+    private MockAdapter mAdapter1;
+    private MockAdapter mAdapter2;
+    private MockAdapter mAdapter3;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mAdapter1 = new MockAdapter(getContext(), Lists.newArrayList("A", "B"));
+        mAdapter2 = new MockAdapter(getContext(), new ArrayList<String>());
+        mAdapter3 = new MockAdapter(getContext(), Lists.newArrayList("C", "D", "E"));
+    }
+
+    public void testGetCount() {
+        CompositeListAdapter adapter = new CompositeListAdapter();
+        adapter.addAdapter(mAdapter1);
+        adapter.addAdapter(mAdapter2);
+        adapter.addAdapter(mAdapter3);
+
+        assertEquals(5, adapter.getCount());
+    }
+
+    public void testGetCountWithInvalidation() {
+        CompositeListAdapter adapter = new CompositeListAdapter();
+        assertEquals(0, adapter.getCount());
+
+        adapter.addAdapter(mAdapter1);
+        assertEquals(2, adapter.getCount());
+
+        adapter.addAdapter(mAdapter2);
+        assertEquals(2, adapter.getCount());
+
+        adapter.addAdapter(mAdapter3);
+        assertEquals(5, adapter.getCount());
+    }
+
+    public void testGetItem() {
+        CompositeListAdapter adapter = new CompositeListAdapter();
+        adapter.addAdapter(mAdapter1);
+        adapter.addAdapter(mAdapter2);
+        adapter.addAdapter(mAdapter3);
+
+        assertEquals("A", adapter.getItem(0));
+        assertEquals("B", adapter.getItem(1));
+        assertEquals("C", adapter.getItem(2));
+        assertEquals("D", adapter.getItem(3));
+        assertEquals("E", adapter.getItem(4));
+    }
+
+    public void testGetItemId() {
+        CompositeListAdapter adapter = new CompositeListAdapter();
+        adapter.addAdapter(mAdapter1);
+        adapter.addAdapter(mAdapter2);
+        adapter.addAdapter(mAdapter3);
+
+        assertEquals(0, adapter.getItemId(0));
+        assertEquals(1, adapter.getItemId(1));
+        assertEquals(0, adapter.getItemId(2));
+        assertEquals(1, adapter.getItemId(3));
+        assertEquals(2, adapter.getItemId(4));
+    }
+
+    public void testGetView() {
+        CompositeListAdapter adapter = new CompositeListAdapter();
+        adapter.addAdapter(mAdapter1);
+        adapter.addAdapter(mAdapter2);
+        adapter.addAdapter(mAdapter3);
+
+        assertEquals(0, adapter.getView(0, null, null).getTag());
+        assertEquals(1, adapter.getView(1, null, null).getTag());
+        assertEquals(0, adapter.getView(2, null, null).getTag());
+        assertEquals(1, adapter.getView(3, null, null).getTag());
+        assertEquals(2, adapter.getView(4, null, null).getTag());
+    }
+
+    public void testGetViewTypeCount() {
+        mAdapter1.viewTypeCount = 2;
+        mAdapter2.viewTypeCount = 3;
+        CompositeListAdapter adapter = new CompositeListAdapter();
+        adapter.addAdapter(mAdapter1);
+        adapter.addAdapter(mAdapter2);
+        adapter.addAdapter(mAdapter3);
+
+        // Note that mAdapter2 adds an implicit +1
+        assertEquals(6, adapter.getViewTypeCount());
+    }
+
+    public void testGetItemViewType() {
+        mAdapter1.viewTypeCount = 2;
+        mAdapter1.viewTypes.put(0, 1);
+        mAdapter1.viewTypes.put(1, 0);
+
+        mAdapter3.viewTypeCount = 3;
+        mAdapter3.viewTypes.put(0, 1);
+        mAdapter3.viewTypes.put(1, 2);
+        mAdapter3.viewTypes.put(2, 0);
+
+        CompositeListAdapter adapter = new CompositeListAdapter();
+        adapter.addAdapter(mAdapter1);
+        adapter.addAdapter(mAdapter2);
+        adapter.addAdapter(mAdapter3);
+
+        assertEquals(1, adapter.getItemViewType(0));
+        assertEquals(0, adapter.getItemViewType(1));
+
+        // Note: mAdapter2 throws in a +1
+
+        assertEquals(4, adapter.getItemViewType(2));
+        assertEquals(5, adapter.getItemViewType(3));
+        assertEquals(3, adapter.getItemViewType(4));
+    }
+
+    public void testNotifyDataSetChangedPropagated() {
+        CompositeListAdapter adapter = new CompositeListAdapter();
+        adapter.addAdapter(mAdapter1);
+        adapter.addAdapter(mAdapter2);
+
+        TestDataSetObserver observer = new TestDataSetObserver();
+        adapter.registerDataSetObserver(observer);
+        mAdapter1.add("X");
+
+        assertEquals(1, observer.changeCount);
+        assertEquals(0, observer.invalidationCount);
+        assertEquals(3, adapter.getCount());
+        assertEquals("A", adapter.getItem(0));
+        assertEquals("B", adapter.getItem(1));
+        assertEquals("X", adapter.getItem(2));
+
+        mAdapter2.add("Y");
+        assertEquals(2, observer.changeCount);
+        assertEquals(0, observer.invalidationCount);
+        assertEquals(4, adapter.getCount());
+        assertEquals("A", adapter.getItem(0));
+        assertEquals("B", adapter.getItem(1));
+        assertEquals("X", adapter.getItem(2));
+        assertEquals("Y", adapter.getItem(3));
+
+    }
+
+    public void testNotifyDataSetChangedOnAddingAdapter() {
+        CompositeListAdapter adapter = new CompositeListAdapter();
+        adapter.addAdapter(mAdapter1);
+
+        TestDataSetObserver observer = new TestDataSetObserver();
+        adapter.registerDataSetObserver(observer);
+        adapter.addAdapter(mAdapter3);
+
+        assertEquals(1, observer.changeCount);
+        assertEquals(0, observer.invalidationCount);
+        assertEquals(5, adapter.getCount());
+        assertEquals("A", adapter.getItem(0));
+        assertEquals("B", adapter.getItem(1));
+        assertEquals("C", adapter.getItem(2));
+        assertEquals("D", adapter.getItem(3));
+        assertEquals("E", adapter.getItem(4));
+    }
+
+    public void testNotifyDataSetInvalidated() {
+        CompositeListAdapter adapter = new CompositeListAdapter();
+        adapter.addAdapter(mAdapter1);
+
+        TestDataSetObserver observer = new TestDataSetObserver();
+        adapter.registerDataSetObserver(observer);
+
+        mAdapter1.remove("A");
+        assertEquals(1, observer.changeCount);
+        assertEquals(0, observer.invalidationCount);
+        assertEquals(1, adapter.getCount());
+
+        mAdapter1.remove("B");
+        assertEquals(1, observer.changeCount);
+        assertEquals(1, observer.invalidationCount);
+        assertEquals(0, adapter.getCount());
+    }
+
+    public void testAreAllItemsEnabled() {
+        CompositeListAdapter adapter = new CompositeListAdapter();
+        adapter.addAdapter(mAdapter1);
+        adapter.addAdapter(mAdapter3);
+
+        assertTrue(adapter.areAllItemsEnabled());
+    }
+
+    public void testAreAllItemsEnabledWithInvalidation() {
+        CompositeListAdapter adapter = new CompositeListAdapter();
+        adapter.addAdapter(mAdapter1);
+        assertTrue(adapter.areAllItemsEnabled());
+
+        mAdapter3.allItemsEnabled = false;
+        adapter.addAdapter(mAdapter3);
+
+        assertFalse(adapter.areAllItemsEnabled());
+    }
+
+    public void testIsEnabled() {
+        mAdapter1.allItemsEnabled = false;
+        mAdapter1.enabledItems.add(1);
+
+        CompositeListAdapter adapter = new CompositeListAdapter();
+        adapter.addAdapter(mAdapter1);
+        adapter.addAdapter(mAdapter2);
+        adapter.addAdapter(mAdapter3);
+
+        assertFalse(adapter.isEnabled(0));
+        assertTrue(adapter.isEnabled(1));
+        assertTrue(adapter.isEnabled(2));
+        assertTrue(adapter.isEnabled(3));
+        assertTrue(adapter.isEnabled(4));
+    }
+
+    public void testIsEnabledWhenAllEnabledAtLeastOneAdapter() {
+        mAdapter1.allItemsEnabled = false;
+        mAdapter1.enabledItems.add(1);
+        mAdapter3.allItemsEnabled = false;
+        mAdapter3.enabledItems.add(1);
+
+        CompositeListAdapter adapter = new CompositeListAdapter();
+        adapter.addAdapter(mAdapter1);
+        adapter.addAdapter(mAdapter3);
+
+        assertFalse(adapter.isEnabled(0));
+        assertTrue(adapter.isEnabled(1));
+        assertFalse(adapter.isEnabled(2));
+        assertTrue(adapter.isEnabled(3));
+        assertFalse(adapter.isEnabled(4));
+    }
+}