am 6a59ee5c: Import revised translations
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 04d0cf4..d920701 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -224,15 +224,20 @@
                 <data android:mimeType="vnd.android.cursor.item/postal-address" android:host="contacts" />
             </intent-filter>
 
+            <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>
 
         <!-- 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>
@@ -347,7 +352,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">
 
@@ -458,6 +463,10 @@
             </intent-filter>
         </activity>
 
+        <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..4dd680f 100644
--- a/res/layout-finger/contacts_list_content.xml
+++ b/res/layout-finger/contacts_list_content.xml
@@ -26,10 +26,16 @@
         class="com.android.contacts.PinnedHeaderListView" 
         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_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_search_content.xml b/res/layout-finger/contacts_search_content.xml
index ae72376..d9479dc 100644
--- a/res/layout-finger/contacts_search_content.xml
+++ b/res/layout-finger/contacts_search_content.xml
@@ -29,7 +29,8 @@
     <ListView
         android:id="@android:id/list"
         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/contact_detail.xml b/res/layout/contact_detail.xml
new file mode 100644
index 0000000..541ba1c
--- /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.
+-->
+
+<com.android.contacts.views.detail.ContactDetailView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/contact_details"
+    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>
+            
+</com.android.contacts.views.detail.ContactDetailView>
+
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/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..692c413 100644
--- a/res/values/ids.xml
+++ b/res/values/ids.xml
@@ -37,7 +37,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 041440a..168880d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -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">%s</xliff:g>\n<xliff:g id="filename" example="foo.vcf">%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">%s</xliff:g> of <xliff:g id="total_number">%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">%s</xliff:g> of <xliff:g id="total_number">%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>
@@ -1134,4 +1141,34 @@
 
     <!-- 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>
+
 </resources>
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/ContactListEmptyView.java b/src/com/android/contacts/ContactListEmptyView.java
new file mode 100644
index 0000000..58573f1
--- /dev/null
+++ b/src/com/android/contacts/ContactListEmptyView.java
@@ -0,0 +1,114 @@
+/*
+ * 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.content.Intent;
+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);
+    }
+
+    protected 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/ContactListItemView.java b/src/com/android/contacts/ContactListItemView.java
index 89e4265..db2bb48 100644
--- a/src/com/android/contacts/ContactListItemView.java
+++ b/src/com/android/contacts/ContactListItemView.java
@@ -31,6 +31,7 @@
 import android.view.Gravity;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.CheckBox;
 import android.widget.ImageView;
 import android.widget.QuickContactBadge;
 import android.widget.TextView;
@@ -79,14 +80,22 @@
     private TextView mDataView;
     private TextView mSnippetView;
     private ImageView mPresenceIcon;
+    // 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 mPhotoViewWidth;
     private int mPhotoViewHeight;
     private int mLine1Height;
     private int mLine2Height;
     private int mLine3Height;
+    private int mChipWidth;
+    private int mChipRightMargin;
+    private int mCheckBoxMargin;
 
     private OnClickListener mCallButtonClickListener;
+    private OnClickListener mCheckBoxClickListener;
 
     public ContactListItemView(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -119,6 +128,12 @@
                 resources.getDimensionPixelOffset(R.dimen.list_item_presence_icon_margin);
         mHeaderTextWidth =
                 resources.getDimensionPixelOffset(R.dimen.list_item_header_text_width);
+        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);
     }
 
     /**
@@ -128,6 +143,9 @@
         mCallButtonClickListener = callButtonClickListener;
     }
 
+    public void setOnCheckBoxClickListener(OnClickListener checkBoxClickListener) {
+        mCheckBoxClickListener = checkBoxClickListener;
+    }
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         // We will match parent's width and wrap content vertically, but make sure
@@ -168,6 +186,14 @@
             mPresenceIcon.measure(0, 0);
         }
 
+        if (isVisible(mChipView)) {
+            mChipView.measure(0, 0);
+        }
+
+        if (isVisible(mCheckBox)) {
+            mCheckBox.measure(0, 0);
+        }
+
         ensurePhotoViewSize();
 
         height = Math.max(height, mPhotoViewHeight);
@@ -209,6 +235,10 @@
 
         // Left side
         int leftBound = mPaddingLeft;
+        if (mChipView != null) {
+            mChipView.layout(leftBound, topBound, leftBound + mChipWidth, height);
+            leftBound += mChipWidth + mChipRightMargin;
+        }
         View photoView = mQuickContact != null ? mQuickContact : mPhotoView;
         if (photoView != null) {
             // Center the photo vertically
@@ -252,7 +282,17 @@
                     rightBound + iconWidth,
                     height);
         }
-
+        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);
+        }
         if (mHorizontalDividerVisible) {
             ensureHorizontalDivider();
             mHorizontalDividerDrawable.setBounds(
@@ -575,6 +615,29 @@
     }
 
     /**
+     * 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.setOnClickListener(mCheckBoxClickListener);
+            addView(mCheckBox);
+        }
+        return mCheckBox;
+    }
+
+    /**
      * Adds or updates the presence icon view.
      */
     public void setPresence(Drawable icon) {
diff --git a/src/com/android/contacts/ContactsListActivity.java b/src/com/android/contacts/ContactsListActivity.java
index 4169e37..be94b7b 100644
--- a/src/com/android/contacts/ContactsListActivity.java
+++ b/src/com/android/contacts/ContactsListActivity.java
@@ -26,11 +26,11 @@
 import com.android.contacts.util.Constants;
 
 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.ProgressDialog;
 import android.app.SearchManager;
 import android.content.AsyncQueryHandler;
 import android.content.ContentResolver;
@@ -38,7 +38,6 @@
 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;
@@ -58,11 +57,9 @@
 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;
@@ -83,7 +80,6 @@
 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;
@@ -92,6 +88,7 @@
 import android.text.TextUtils;
 import android.text.TextWatcher;
 import android.util.Log;
+import android.util.SparseIntArray;
 import android.view.ContextMenu;
 import android.view.ContextThemeWrapper;
 import android.view.KeyEvent;
@@ -102,16 +99,20 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewStub;
 import android.view.ContextMenu.ContextMenuInfo;
 import android.view.View.OnClickListener;
 import android.view.View.OnFocusChangeListener;
 import android.view.View.OnTouchListener;
+import android.view.animation.AnimationUtils;
 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.BaseAdapter;
 import android.widget.Button;
+import android.widget.CheckBox;
 import android.widget.CursorAdapter;
 import android.widget.Filter;
 import android.widget.ImageView;
@@ -124,6 +125,8 @@
 
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Random;
 
@@ -135,10 +138,6 @@
         View.OnClickListener, View.OnKeyListener, TextWatcher, TextView.OnEditorActionListener,
         OnFocusChangeListener, OnTouchListener {
 
-    public static class JoinContactActivity extends ContactsListActivity {
-
-    }
-
     public static class ContactsSearchActivity extends ContactsListActivity {
 
     }
@@ -167,33 +166,6 @@
 
     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 =
@@ -264,10 +236,6 @@
     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;
@@ -281,13 +249,17 @@
             | MODE_MASK_PICKER | MODE_MASK_SHOW_NUMBER_OF_CONTACTS;
 
     /**
+     * Show all phone numbers and do multiple pick when clicking. This mode has phone filtering
+     * feature, but doesn't support 'search for all contacts'.
+     */
+    static final int MODE_PICK_MULTIPLE_PHONES = 80 | MODE_MASK_PICKER
+            | MODE_MASK_NO_PRESENCE | MODE_MASK_SHOW_PHOTOS | MODE_MASK_DISABLE_QUIKCCONTACT;
+
+    /**
      * 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;
+    protected static final String ACTION_SEARCH_INTERNAL = "com.android.contacts.INTERNAL_SEARCH";
 
     static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] {
         Contacts._ID,                       // 0
@@ -364,6 +336,8 @@
         Phone.NUMBER, //3
         Phone.DISPLAY_NAME, // 4
         Phone.CONTACT_ID, // 5
+        Contacts.SORT_KEY_PRIMARY, // 6
+        Contacts.PHOTO_ID, // 7
     };
     static final String[] LEGACY_PHONES_PROJECTION = new String[] {
         Phones._ID, //0
@@ -378,6 +352,8 @@
     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 int PHONE_SORT_KEY_PRIMARY_COLUMN_INDEX = 6;
+    static final int PHONE_PHOTO_ID_COLUMN_INDEX = 7;
 
     static final String[] POSTALS_PROJECTION = new String[] {
         StructuredPostal._ID, //0
@@ -409,10 +385,15 @@
 
     static final String KEY_PICKER_MODE = "picker_mode";
 
+    private static final String TEL_SCHEME = "tel";
+    private static final String CONTENT_SCHEME = "content";
+
     private ContactItemListAdapter mAdapter;
+    private ContactListEmptyView mEmptyView;
 
     int mMode = MODE_DEFAULT;
 
+    private boolean mRunQueriesSynchronously;
     private QueryHandler mQueryHandler;
     private boolean mJustCreated;
     private boolean mSyncEnabled;
@@ -423,8 +404,6 @@
 
     private Uri mGroupUri;
 
-    private long mQueryAggregateId;
-
     private ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>();
     private int  mWritableSourcesCnt;
     private int  mReadOnlySourcesCnt;
@@ -459,16 +438,6 @@
     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;
@@ -480,6 +449,57 @@
             Contacts.LOOKUP_KEY
     };
 
+    /**
+     * User selected phone number and id in MODE_PICK_MULTIPLE_PHONES mode.
+     */
+    private UserSelection mUserSelection = new UserSelection(null, null);
+
+    /**
+     * The adapter for the phone numbers, used in MODE_PICK_MULTIPLE_PHONES mode.
+     */
+    private PhoneNumberAdapter mPhoneNumberAdapter = new PhoneNumberAdapter(this, null);
+
+    private static 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,
+    };
+
+    /**
+     * This is the map from contact to color index.
+     * A colored chip in MODE_PICK_MULTIPLE_PHONES mode is used to indicate the number of phone
+     * numbers belong to one contact
+     */
+    SparseIntArray mContactColor;
+
+    /**
+     * UI control of action panel in MODE_PICK_MULTIPLE_PHONES mode.
+     */
+    private View mFooterView;
+
+    /**
+     * Display only selected recipients or not in MODE_PICK_MULTIPLE_PHONES mode
+     */
+    private boolean mShowSelectedOnly = false;
+
     static {
         sContactsIdMatcher = new UriMatcher(UriMatcher.NO_MATCH);
         sContactsIdMatcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID);
@@ -557,6 +577,26 @@
         }
     };
 
+    private OnClickListener mCheckBoxClickerListener = new OnClickListener () {
+        public void onClick(View v) {
+            final ContactListItemCache cache = (ContactListItemCache) v.getTag();
+            if (cache.phoneId != PhoneNumberAdapter.INVALID_PHONE_ID) {
+                mUserSelection.setPhoneSelected(cache.phoneId, ((CheckBox) v).isChecked());
+            } else {
+                mUserSelection.setPhoneSelected(cache.phoneNumber,
+                        ((CheckBox) v).isChecked());
+            }
+            updateWidgets(true);
+        }
+    };
+
+    /**
+     * Visible for testing: makes queries run on the UI thread.
+     */
+    /* package */ void runQueriesSynchronously() {
+        mRunQueriesSynchronously = true;
+    }
+
     @Override
     protected void onCreate(Bundle icicle) {
         super.onCreate(icicle);
@@ -565,9 +605,18 @@
         mContactsPrefs = new ContactsPreferences(this);
         mPhotoLoader = new ContactPhotoLoader(this, R.drawable.ic_contact_list_picture);
 
+        mQueryHandler = new QueryHandler(this);
+        mJustCreated = true;
+        mSyncEnabled = true;
+
         // Resolve the intent
         final Intent intent = getIntent();
 
+        resolveIntent(intent);
+        initContentView();
+    }
+
+    protected void resolveIntent(final Intent intent) {
         // 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) {
@@ -576,6 +625,7 @@
 
         String action = intent.getAction();
         String component = intent.getComponent().getClassName();
+        String type = intent.getType();
 
         // 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
@@ -596,6 +646,11 @@
                 if (originalComponent != null) {
                     component = originalComponent;
                 }
+                String originalType =
+                    extras.getString(ContactsSearchManager.ORIGINAL_TYPE_EXTRA_KEY);
+                if (originalType != null) {
+                    type = originalType;
+                }
             } else {
                 mInitialFilter = null;
             }
@@ -630,18 +685,19 @@
         } 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)) {
+           // TODO : Does it work in mSearchMode?
+            final String resolvedType = intent.resolveType(this);
+            if (Contacts.CONTENT_TYPE.equals(resolvedType)) {
                 mMode = MODE_PICK_CONTACT;
-            } else if (People.CONTENT_TYPE.equals(type)) {
+            } else if (People.CONTENT_TYPE.equals(resolvedType)) {
                 mMode = MODE_LEGACY_PICK_PERSON;
-            } else if (Phone.CONTENT_TYPE.equals(type)) {
+            } else if (Phone.CONTENT_TYPE.equals(resolvedType)) {
                 mMode = MODE_PICK_PHONE;
-            } else if (Phones.CONTENT_TYPE.equals(type)) {
+            } else if (Phones.CONTENT_TYPE.equals(resolvedType)) {
                 mMode = MODE_LEGACY_PICK_PHONE;
-            } else if (StructuredPostal.CONTENT_TYPE.equals(type)) {
+            } else if (StructuredPostal.CONTENT_TYPE.equals(resolvedType)) {
                 mMode = MODE_PICK_POSTAL;
-            } else if (ContactMethods.CONTENT_POSTAL_TYPE.equals(type)) {
+            } else if (ContactMethods.CONTENT_POSTAL_TYPE.equals(resolvedType)) {
                 mMode = MODE_LEGACY_PICK_POSTAL;
             }
         } else if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) {
@@ -665,22 +721,23 @@
                 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)) {
+            // TODO : Does it work in mSearchMode?
+            final String resolvedType = intent.resolveType(this);
+            if (Contacts.CONTENT_ITEM_TYPE.equals(resolvedType)) {
                 if (mSearchMode) {
                     mMode = MODE_PICK_CONTACT;
                 } else {
                     mMode = MODE_PICK_OR_CREATE_CONTACT;
                 }
-            } else if (Phone.CONTENT_ITEM_TYPE.equals(type)) {
+            } else if (Phone.CONTENT_ITEM_TYPE.equals(resolvedType)) {
                 mMode = MODE_PICK_PHONE;
-            } else if (Phones.CONTENT_ITEM_TYPE.equals(type)) {
+            } else if (Phones.CONTENT_ITEM_TYPE.equals(resolvedType)) {
                 mMode = MODE_LEGACY_PICK_PHONE;
-            } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(type)) {
+            } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(resolvedType)) {
                 mMode = MODE_PICK_POSTAL;
-            } else if (ContactMethods.CONTENT_POSTAL_ITEM_TYPE.equals(type)) {
+            } else if (ContactMethods.CONTENT_POSTAL_ITEM_TYPE.equals(resolvedType)) {
                 mMode = MODE_LEGACY_PICK_POSTAL;
-            }  else if (People.CONTENT_ITEM_TYPE.equals(type)) {
+            }  else if (People.CONTENT_ITEM_TYPE.equals(resolvedType)) {
                 if (mSearchMode) {
                     mMode = MODE_LEGACY_PICK_PERSON;
                 } else {
@@ -783,23 +840,23 @@
             startActivity(newIntent);
             finish();
             return;
-        }
-
-        if (JOIN_AGGREGATE.equals(action)) {
+        } else if (JoinContactActivity.JOIN_CONTACT.equals(action)) {
+            mMode = MODE_PICK_CONTACT;
+        } else if (Intents.ACTION_GET_MULTIPLE_PHONES.equals(action)) {
             if (mSearchMode) {
-                mMode = MODE_PICK_CONTACT;
+                mShowSearchSnippets = false;
+            }
+            if (Phone.CONTENT_TYPE.equals(type)) {
+                mMode = MODE_PICK_MULTIPLE_PHONES;
+                mContactColor = new SparseIntArray();
+                initMultiPicker(intent);
             } 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();
-                }
+                // TODO support other content types
+                Log.e(TAG, "Intent " + action + " is not supported for type " + type);
+                setResult(RESULT_CANCELED);
+                finish();
             }
         }
-
         if (mMode == MODE_UNKNOWN) {
             mMode = MODE_DEFAULT;
         }
@@ -808,16 +865,10 @@
                 && !mSearchResultsMode) {
             mShowNumberOfContacts = true;
         }
+    }
 
-        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) {
+    public void initContentView() {
+        if (mSearchMode) {
             setContentView(R.layout.contacts_search_content);
         } else if (mSearchResultsMode) {
             setContentView(R.layout.contacts_list_search_results);
@@ -828,15 +879,28 @@
             setContentView(R.layout.contacts_list_content);
         }
 
-        setupListView();
+        setupListView(new ContactItemListAdapter(this));
         if (mSearchMode) {
             setupSearchView();
         }
 
-        mQueryHandler = new QueryHandler(this);
-        mJustCreated = true;
+        if (mMode == MODE_PICK_MULTIPLE_PHONES) {
+            ViewStub stub = (ViewStub)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);
+            }
+        }
 
-        mSyncEnabled = true;
+        View emptyView = mList.getEmptyView();
+        if (emptyView instanceof ContactListEmptyView) {
+            mEmptyView = (ContactListEmptyView)emptyView;
+        }
     }
 
     /**
@@ -856,7 +920,7 @@
         getContentResolver().unregisterContentObserver(mProviderStatusObserver);
     }
 
-    private void setupListView() {
+    protected void setupListView(ContactItemListAdapter adapter) {
         final ListView list = getListView();
         final LayoutInflater inflater = getLayoutInflater();
 
@@ -868,7 +932,7 @@
         list.setDividerHeight(0);
         list.setOnCreateContextMenuListener(this);
 
-        mAdapter = new ContactItemListAdapter(this);
+        mAdapter = adapter;
         setListAdapter(mAdapter);
 
         if (list instanceof PinnedHeaderListView && mAdapter.getDisplaySectionHeadersEnabled()) {
@@ -897,29 +961,6 @@
         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);
-            }
-        } finally {
-            if (c != null) {
-                c.close();
-            }
-        }
-
-        if (contactName == null) {
-            contactName = "";
-        }
-
-        return contactName;
-    }
-
     private int getSummaryDisplayNameColumnIndex() {
         if (mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
             return SUMMARY_DISPLAY_NAME_PRIMARY_COLUMN_INDEX;
@@ -942,66 +983,16 @@
                 }
                 break;
             }
+            case R.id.done:
+                setMultiPickerResult();
+                finish();
+                break;
+            case R.id.revert:
+                finish();
+                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);
     }
@@ -1081,48 +1072,49 @@
 
         // 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;
+        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_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_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;
+                            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();
             }
-        } finally {
-            cursor.close();
         }
 
         importFailureView.setVisibility(
@@ -1159,7 +1151,7 @@
         retryUpgrade.setOnClickListener(listener);
     }
 
-    private String getTextFilter() {
+    protected String getTextFilter() {
         if (mSearchEditText != null) {
             return mSearchEditText.getText().toString();
         }
@@ -1191,6 +1183,9 @@
         // 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());
+            if (mMode == MODE_PICK_MULTIPLE_PHONES && mUserSelection != null) {
+                mUserSelection.saveInstanceState(icicle);
+            }
         }
     }
 
@@ -1199,13 +1194,15 @@
         super.onRestoreInstanceState(icicle);
         // Retrieve list state. This will be applied after the QueryHandler has run
         mListState = icicle.getParcelable(LIST_STATE_KEY);
+        if (mMode == MODE_PICK_MULTIPLE_PHONES) {
+            mUserSelection = new UserSelection(icicle);
+        }
     }
 
     @Override
     protected void onStop() {
         super.onStop();
 
-        mAdapter.setSuggestionsCursor(null);
         mAdapter.changeCursor(null);
 
         if (mMode == MODE_QUERY) {
@@ -1219,6 +1216,12 @@
     public boolean onCreateOptionsMenu(Menu menu) {
         super.onCreateOptionsMenu(menu);
 
+        if (mMode == MODE_PICK_MULTIPLE_PHONES) {
+            final MenuInflater inflater = getMenuInflater();
+            inflater.inflate(R.menu.pick, menu);
+            return true;
+        }
+
         // 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) {
@@ -1232,6 +1235,26 @@
 
     @Override
     public boolean onPrepareOptionsMenu(Menu menu) {
+        if (mMode == MODE_PICK_MULTIPLE_PHONES) {
+            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;
+        }
+
         final boolean defaultMode = (mMode == MODE_DEFAULT);
         menu.findItem(R.id.menu_display_groups).setVisible(defaultMode);
         return true;
@@ -1266,6 +1289,28 @@
                 startActivity(intent);
                 return true;
             }
+            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 false;
     }
@@ -1282,8 +1327,16 @@
         } else {
             if (!mSearchMode && (mMode & MODE_MASK_NO_FILTER) == 0) {
                 if ((mMode & MODE_MASK_PICKER) != 0) {
+                    Bundle extras = null;
+                    if (mMode == MODE_PICK_MULTIPLE_PHONES) {
+                        extras = getIntent().getExtras();
+                        if (extras == null) {
+                            extras = new Bundle();
+                        }
+                        mUserSelection.fillSelectionForSearchMode(extras);
+                    }
                     ContactsSearchManager.startSearchForResult(this, initialQuery,
-                            SUBACTIVITY_FILTER);
+                            SUBACTIVITY_FILTER, extras);
                 } else {
                     ContactsSearchManager.startSearch(this, initialQuery);
                 }
@@ -1296,9 +1349,6 @@
      * search text edit.
      */
     protected void onSearchTextChanged() {
-        // Set the proper empty string
-        setEmptyText();
-
         Filter filter = mAdapter.getFilter();
         filter.filter(getTextFilter());
     }
@@ -1306,7 +1356,7 @@
     /**
      * Starts a new activity that will run a search query and display search results.
      */
-    private void doSearch() {
+    protected void doSearch() {
         String query = getTextFilter();
         if (TextUtils.isEmpty(query)) {
             return;
@@ -1549,6 +1599,10 @@
                 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;
         }
@@ -1730,6 +1784,14 @@
         return false;
     }
 
+    @Override
+    public void onBackPressed() {
+        if (mMode == MODE_PICK_MULTIPLE_PHONES) {
+            setMultiPickerResult();
+        }
+        super.onBackPressed();
+    }
+
     /**
      * Prompt the user before deleting the given {@link Contacts} entry.
      */
@@ -1808,6 +1870,10 @@
     protected void onListItemClick(ListView l, View v, int position, long id) {
         hideSoftKeyboard();
 
+        onListItemClick(position, id);
+    }
+
+    protected void onListItemClick(int position, long id) {
         if (mSearchMode && mAdapter.isSearchAllContactsItemPosition(position)) {
             doSearch();
         } else if (mMode == MODE_INSERT_OR_EDIT_CONTACT || mMode == MODE_QUERY_PICK_TO_EDIT) {
@@ -1830,16 +1896,11 @@
                 && 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);
@@ -1872,7 +1933,7 @@
      * @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) {
+    protected void returnPickerResult(Cursor c, String name, Uri selectedUri) {
         final Intent intent = new Intent();
 
         if (mShortcutAction != null) {
@@ -2078,10 +2139,8 @@
         }
     }
 
-    private Uri getUriToQuery() {
+    protected Uri getUriToQuery() {
         switch(mMode) {
-            case MODE_JOIN_CONTACT:
-                return getJoinSuggestionsUri(null);
             case MODE_FREQUENT:
             case MODE_STARRED:
                 return Contacts.CONTENT_URI;
@@ -2099,6 +2158,7 @@
             case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
                 return People.CONTENT_URI;
             }
+            case MODE_PICK_MULTIPLE_PHONES:
             case MODE_PICK_PHONE: {
                 return buildSectionIndexerUri(Phone.CONTENT_URI);
             }
@@ -2173,7 +2233,7 @@
      * 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) {
+    protected Uri getSelectedUri(int position) {
         if (position == ListView.INVALID_POSITION) {
             throw new IllegalArgumentException("Position not in list bounds");
         }
@@ -2205,7 +2265,6 @@
 
     String[] getProjectionForQuery() {
         switch(mMode) {
-            case MODE_JOIN_CONTACT:
             case MODE_STREQUENT:
             case MODE_FREQUENT:
             case MODE_STARRED:
@@ -2228,6 +2287,7 @@
                 return LEGACY_PEOPLE_PROJECTION ;
             }
             case MODE_QUERY_PICK_PHONE:
+            case MODE_PICK_MULTIPLE_PHONES:
             case MODE_PICK_PHONE: {
                 return PHONES_PROJECTION;
             }
@@ -2314,7 +2374,7 @@
         }
     }
 
-    private Uri getContactFilterUri(String filter) {
+    protected Uri getContactFilterUri(String filter) {
         Uri baseUri;
         if (!TextUtils.isEmpty(filter)) {
             baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(filter));
@@ -2342,39 +2402,40 @@
                 .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) {
+    protected String getSortOrder(String[] projectionType) {
+        String sortKey;
         if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
-            return Contacts.SORT_KEY_PRIMARY;
+            sortKey = Contacts.SORT_KEY_PRIMARY;
         } else {
-            return Contacts.SORT_KEY_ALTERNATIVE;
+            sortKey = Contacts.SORT_KEY_ALTERNATIVE;
         }
+        switch (mMode) {
+            case MODE_LEGACY_PICK_PERSON:
+            case MODE_LEGACY_PICK_OR_CREATE_PERSON:
+                sortKey = Contacts.DISPLAY_NAME;
+                break;
+            case MODE_LEGACY_PICK_PHONE:
+                sortKey = People.DISPLAY_NAME;
+                break;
+        }
+        return sortKey;
     }
 
     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);
         }
 
+        if (mEmptyView != null) {
+            mEmptyView.hide();
+        }
+
         mAdapter.setLoading(true);
 
         // Cancel any pending queries
         mQueryHandler.cancelOperation(QUERY_TOKEN);
-        mQueryHandler.setLoadingJoinSuggestions(false);
 
         mSortOrder = mContactsPrefs.getSortOrder();
         mDisplayOrder = mContactsPrefs.getDisplayOrder();
@@ -2405,6 +2466,10 @@
                     .build();
         }
 
+        startQuery(uri, projection);
+    }
+
+    protected void startQuery(Uri uri, String[] projection) {
         // Kick off the new query
         switch (mMode) {
             case MODE_GROUP:
@@ -2447,6 +2512,25 @@
                 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, null, null, null);
                 break;
 
+            case MODE_PICK_MULTIPLE_PHONES:
+                // 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));
+                    break;
+                }
+                // Fall through For other cases
             case MODE_PICK_PHONE:
             case MODE_LEGACY_PICK_PHONE:
                 mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
@@ -2459,15 +2543,15 @@
                         ContactMethods.KIND + "=" + android.provider.Contacts.KIND_POSTAL, null,
                         getSortOrder(projection));
                 break;
-
-            case MODE_JOIN_CONTACT:
-                mQueryHandler.setLoadingJoinSuggestions(true);
-                mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection,
-                        null, null, null);
-                break;
         }
     }
 
+    protected void startQuery(Uri uri, String[] projection, String selection,
+            String[] selectionArgs, String sortOrder) {
+        mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, selection, selectionArgs,
+                sortOrder);
+    }
+
     /**
      * Called from a background thread to do the filter and return the resulting cursor.
      *
@@ -2520,6 +2604,10 @@
                 return resolver.query(uri, projection, null, null, null);
             }
 
+            case MODE_PICK_MULTIPLE_PHONES:
+                // Filter phone numbers as well.
+                mPhoneNumberAdapter.doFilter(filter, mShowSelectedOnly);
+                // Fall through
             case MODE_PICK_PHONE: {
                 Uri uri = getUriToQuery();
                 if (!TextUtils.isEmpty(filter)) {
@@ -2533,30 +2621,10 @@
                 //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.
@@ -2695,56 +2763,173 @@
         return (Cursor) listView.getAdapter().getItem(index);
     }
 
-    private static class QueryHandler extends AsyncQueryHandler {
+    private void initMultiPicker(final Intent intent) {
+        final Handler handler = new Handler();
+        // TODO : Shall we still show the progressDialog in search mode.
+        final ProgressDialog progressDialog = new ProgressDialog(this);
+        progressDialog.setMessage(getText(R.string.adding_recipients));
+        progressDialog.setIndeterminate(true);
+        progressDialog.setCancelable(false);
+
+        final Runnable showProgress = new Runnable() {
+            public void run() {
+                progressDialog.show();
+            }
+        };
+        handler.postDelayed(showProgress, 1);
+
+        new Thread(new Runnable() {
+            public void run() {
+                try {
+                    loadSelectionFromIntent(intent);
+                } finally {
+                    handler.removeCallbacks(showProgress);
+                    progressDialog.dismiss();
+                }
+                final Runnable populateWorker = new Runnable() {
+                    public void run() {
+                        if (mAdapter != null) {
+                            mAdapter.notifyDataSetChanged();
+                        }
+                        updateWidgets(false);
+                    }
+                };
+                handler.post(populateWorker);
+            }
+        }).start();
+    }
+
+    private void getPhoneNumbersOrIdsFromURIs(final Parcelable[] uris,
+            final List<String> phoneNumbers, final List<Long> phoneIds) {
+        if (uris != null) {
+            for (Parcelable paracelable : uris) {
+                Uri uri = (Uri) paracelable;
+                if (uri == null) continue;
+                String scheme = uri.getScheme();
+                if (phoneNumbers != null && TEL_SCHEME.equals(scheme)) {
+                    phoneNumbers.add(uri.getSchemeSpecificPart());
+                } else if (phoneIds != null && CONTENT_SCHEME.equals(scheme)) {
+                    phoneIds.add(ContentUris.parseId(uri));
+                }
+            }
+        }
+    }
+
+    private void loadSelectionFromIntent(Intent intent) {
+        Parcelable[] uris = intent.getParcelableArrayExtra(Intents.EXTRA_PHONE_URIS);
+        ArrayList<String> phoneNumbers = new ArrayList<String>();
+        ArrayList<Long> phoneIds = new ArrayList<Long>();
+        ArrayList<String> selectedPhoneNumbers = null;
+        if (mSearchMode) {
+            // All selection will be read from EXTRA_SELECTION
+            getPhoneNumbersOrIdsFromURIs(uris, phoneNumbers, null);
+            uris = intent.getParcelableArrayExtra(UserSelection.EXTRA_SELECTION);
+            if (uris != null) {
+                selectedPhoneNumbers = new ArrayList<String>();
+                getPhoneNumbersOrIdsFromURIs(uris, selectedPhoneNumbers, phoneIds);
+            }
+        } else {
+            getPhoneNumbersOrIdsFromURIs(uris, phoneNumbers, phoneIds);
+            selectedPhoneNumbers = phoneNumbers;
+        }
+        mPhoneNumberAdapter = new PhoneNumberAdapter(this, phoneNumbers);
+        mUserSelection = new UserSelection(selectedPhoneNumbers, phoneIds);
+    }
+
+    private void setMultiPickerResult() {
+        setResult(RESULT_OK, mUserSelection.createSelectionIntent());
+    }
+
+    /**
+     * Go through the cursor and assign the chip color to contact who has more than one phone
+     * numbers.
+     * Assume the cursor is sorted by CONTACT_ID.
+     */
+    private void updateChipColor(Cursor cursor) {
+        if (cursor == null || cursor.getCount() == 0) {
+            return;
+        }
+        mContactColor.clear();
+        int backupPos = cursor.getPosition();
+        cursor.moveToFirst();
+        int color = 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(Long.valueOf(contactId).hashCode()) < 0) {
+                    mContactColor.put(Long.valueOf(contactId).hashCode(), CHIP_COLOR_ARRAY[color]);
+                    color++;
+                    if (color >= CHIP_COLOR_ARRAY.length) {
+                        color = 0;
+                    }
+                }
+            }
+            prevContactId = contactId;
+        }
+        cursor.moveToPosition(backupPos);
+    }
+
+    /**
+     * Get assigned chip color resource id for a given contact, 0 is returned if there is no mapped
+     * resource.
+     */
+    private int getChipColor(long contactId) {
+        return mContactColor.get(Long.valueOf(contactId).hashCode());
+    }
+
+    private void updateWidgets(boolean changed) {
+        int selected = mUserSelection.selectedCount();
+
+        if (selected >= 1) {
+            final String format =
+                getResources().getQuantityString(R.plurals.multiple_picker_title, selected);
+            setTitle(String.format(format, selected));
+        } else {
+            setTitle(getString(R.string.contactsList));
+        }
+
+        if (changed && mFooterView.getVisibility() == View.GONE) {
+            mFooterView.setVisibility(View.VISIBLE);
+            mFooterView.startAnimation(AnimationUtils.loadAnimation(this, R.anim.footer_appear));
+        }
+    }
+
+    private void checkAll(boolean checked) {
+        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);
+        }
+    }
+
+    private 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
+        public void startQuery(int token, Object cookie, Uri uri, String[] projection,
+                String selection, String[] selectionArgs, String orderBy) {
+            final ContactsListActivity activity = mActivity.get();
+            if (activity != null && activity.mRunQueriesSynchronously) {
+                Cursor cursor = getContentResolver().query(uri, projection, selection,
+                        selectionArgs, orderBy);
+                onQueryComplete(token, cookie, cursor);
+            } else {
+                super.startQuery(token, cookie, uri, projection, selection, selectionArgs, orderBy);
+            }
         }
 
         @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;
-                }
+                activity.onQueryComplete(cursor);
             } else {
                 if (cursor != null) {
                     cursor.close();
@@ -2753,12 +2938,25 @@
         }
     }
 
+    protected void onQueryComplete(Cursor cursor) {
+        mAdapter.changeCursor(cursor);
+
+        // Now that the cursor is populated again, it's possible to restore the list state
+        if (mListState != null) {
+            mList.onRestoreInstanceState(mListState);
+            mListState = null;
+        }
+    }
+
     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);
+        public long phoneId;
+        // phoneNumber only validates when phoneId = INVALID_PHONE_ID
+        public String phoneNumber;
     }
 
     final static class PinnedHeaderCache {
@@ -2767,7 +2965,7 @@
         public Drawable background;
     }
 
-    private final class ContactItemListAdapter extends CursorAdapter
+    protected class ContactItemListAdapter extends CursorAdapter
             implements SectionIndexer, OnScrollListener, PinnedHeaderListView.PinnedHeaderAdapter {
         private SectionIndexer mIndexer;
         private boolean mLoading = true;
@@ -2777,8 +2975,6 @@
         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);
@@ -2824,14 +3020,6 @@
             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
@@ -2882,10 +3070,6 @@
                 return IGNORE_ITEM_VIEW_TYPE;
             }
 
-            if (isShowAllContactsItemPosition(position)) {
-                return IGNORE_ITEM_VIEW_TYPE;
-            }
-
             if (isSearchAllContactsItemPosition(position)) {
                 return IGNORE_ITEM_VIEW_TYPE;
             }
@@ -2894,7 +3078,9 @@
                 // We don't want the separator view to be recycled.
                 return IGNORE_ITEM_VIEW_TYPE;
             }
-
+            if (mMode == MODE_PICK_MULTIPLE_PHONES && position < mPhoneNumberAdapter.getCount()) {
+                return mPhoneNumberAdapter.getItemViewType(position);
+            }
             return super.getItemViewType(position);
         }
 
@@ -2915,11 +3101,6 @@
                 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);
@@ -2934,18 +3115,13 @@
                 return view;
             }
 
-            boolean showingSuggestion;
-            Cursor cursor;
-            if (mSuggestionsCursorCount != 0 && position < mSuggestionsCursorCount + 2) {
-                showingSuggestion = true;
-                cursor = mSuggestionsCursor;
-            } else {
-                showingSuggestion = false;
-                cursor = mCursor;
+            // Check whether this view should be retrieved from mPhoneNumberAdapter
+            if (mMode == MODE_PICK_MULTIPLE_PHONES && position < mPhoneNumberAdapter.getCount()) {
+                return mPhoneNumberAdapter.getView(position, convertView, parent);
             }
 
             int realPosition = getRealPosition(position);
-            if (!cursor.moveToPosition(realPosition)) {
+            if (!mCursor.moveToPosition(realPosition)) {
                 throw new IllegalStateException("couldn't move cursor to position " + position);
             }
 
@@ -2953,13 +3129,13 @@
             View v;
             if (convertView == null || convertView.getTag() == null) {
                 newView = true;
-                v = newView(mContext, cursor, parent);
+                v = newView(mContext, mCursor, parent);
             } else {
                 newView = false;
                 v = convertView;
             }
-            bindView(v, mContext, cursor);
-            bindSectionHeader(v, realPosition, mDisplaySectionHeaders && !showingSuggestion);
+            bindView(v, mContext, mCursor);
+            bindSectionHeader(v, realPosition, mDisplaySectionHeaders);
             return v;
         }
 
@@ -2988,13 +3164,8 @@
             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;
+            return mSearchMode && mMode != MODE_PICK_MULTIPLE_PHONES && position == getCount() - 1;
         }
 
         private int getSeparatorId(int position) {
@@ -3002,13 +3173,6 @@
             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;
         }
 
@@ -3016,6 +3180,7 @@
         public View newView(Context context, Cursor cursor, ViewGroup parent) {
             final ContactListItemView view = new ContactListItemView(context, null);
             view.setOnCallButtonClickListener(ContactsListActivity.this);
+            view.setOnCheckBoxClickListener(mCheckBoxClickerListener);
             view.setTag(new ContactListItemCache());
             return view;
         }
@@ -3031,9 +3196,11 @@
             int defaultType;
             int nameColumnIndex;
             int phoneticNameColumnIndex;
+            int photoColumnIndex = SUMMARY_PHOTO_ID_COLUMN_INDEX;
             boolean displayAdditionalData = mDisplayAdditionalData;
             boolean highlightingEnabled = false;
             switch(mMode) {
+                case MODE_PICK_MULTIPLE_PHONES:
                 case MODE_PICK_PHONE:
                 case MODE_LEGACY_PICK_PHONE:
                 case MODE_QUERY_PICK_PHONE: {
@@ -3043,6 +3210,7 @@
                     typeColumnIndex = PHONE_TYPE_COLUMN_INDEX;
                     labelColumnIndex = PHONE_LABEL_COLUMN_INDEX;
                     defaultType = Phone.TYPE_HOME;
+                    photoColumnIndex = PHONE_PHOTO_ID_COLUMN_INDEX;
                     break;
                 }
                 case MODE_PICK_POSTAL:
@@ -3067,6 +3235,15 @@
                 }
             }
 
+            if (mMode == MODE_PICK_MULTIPLE_PHONES) {
+                cache.phoneId = Long.valueOf(cursor.getLong(PHONE_ID_COLUMN_INDEX));
+                CheckBox checkBox = view.getCheckBoxView();
+                checkBox.setChecked(mUserSelection.isSelected(cache.phoneId));
+                checkBox.setTag(cache);
+                int color = getChipColor(cursor.getLong(PHONE_CONTACT_ID_COLUMN_INDEX));
+                view.getChipView().setBackgroundResource(color);
+            }
+
             // Set the name
             cursor.copyStringToBuffer(nameColumnIndex, cache.nameBuffer);
             TextView nameView = view.getNameTextView();
@@ -3086,11 +3263,9 @@
                 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) {
+            if (mDisplayCallButton && cursor.getColumnCount() > SUMMARY_HAS_PHONE_COLUMN_INDEX
+                    && cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0) {
                 int pos = cursor.getPosition();
                 view.showCallButton(android.R.id.button1, pos);
             } else {
@@ -3102,8 +3277,8 @@
                 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);
+                if (!cursor.isNull(photoColumnIndex)) {
+                    photoId = cursor.getLong(photoColumnIndex);
                 }
 
                 ImageView viewToUse;
@@ -3236,7 +3411,7 @@
             textView.setText(textWithHighlighting);
         }
 
-        private void bindSectionHeader(View itemView, int position, boolean displaySectionHeaders) {
+        protected void bindSectionHeader(View itemView, int position, boolean displaySectionHeaders) {
             final ContactListItemView view = (ContactListItemView)itemView;
             final ContactListItemCache cache = (ContactListItemCache) view.getTag();
             if (!displaySectionHeaders) {
@@ -3292,9 +3467,25 @@
                 foundContactsText.setText(text);
             }
 
+            if (mEmptyView != null && (cursor == null || cursor.getCount() == 0)) {
+                mEmptyView.show(mSearchMode, mDisplayOnlyPhones,
+                        mMode == MODE_STREQUENT || mMode == MODE_STARRED,
+                        mMode == MODE_QUERY || mMode == MODE_QUERY_PICK
+                        || mMode == MODE_QUERY_PICK_PHONE || mMode == MODE_QUERY_PICK_TO_VIEW
+                        || mMode == MODE_QUERY_PICK_TO_EDIT,
+                        mShortcutAction != null,
+                        mMode == MODE_PICK_MULTIPLE_PHONES,
+                        mShowSelectedOnly);
+            }
+
             super.changeCursor(cursor);
+
             // Update the indexer for the fast scroll widget
             updateIndexer(cursor);
+
+            if (mMode == MODE_PICK_MULTIPLE_PHONES) {
+                updateChipColor(cursor);
+            }
         }
 
         private void updateIndexer(Cursor cursor) {
@@ -3350,8 +3541,7 @@
         @Override
         public boolean areAllItemsEnabled() {
             return mMode != MODE_STARRED
-                && !mShowNumberOfContacts
-                && mSuggestionsCursorCount == 0;
+                && !mShowNumberOfContacts;
         }
 
         @Override
@@ -3362,10 +3552,6 @@
                 }
                 position--;
             }
-
-            if (mSuggestionsCursorCount > 0) {
-                return position != 0 && position != mSuggestionsCursorCount + 1;
-            }
             return position != mFrequentSeparatorPos;
         }
 
@@ -3382,7 +3568,7 @@
                 superCount++;
             }
 
-            if (mSearchMode) {
+            if (mSearchMode && mMode != MODE_PICK_MULTIPLE_PHONES) {
                 // Last element in the list is the "Find
                 superCount++;
             }
@@ -3393,12 +3579,11 @@
                 superCount++;
             }
 
-            if (mSuggestionsCursorCount != 0) {
-                // When showing suggestions, we have 2 additional list items: the "Suggestions"
-                // and "All contacts" headers.
-                return mSuggestionsCursorCount + superCount + 2;
+            if (mMode == MODE_PICK_MULTIPLE_PHONES) {
+                superCount += mPhoneNumberAdapter.getCount();
             }
-            else if (mFrequentSeparatorPos != ListView.INVALID_POSITION) {
+
+            if (mFrequentSeparatorPos != ListView.INVALID_POSITION) {
                 // When showing strequent list, we have an additional list item - the separator.
                 return superCount + 1;
             } else {
@@ -3420,19 +3605,13 @@
 
             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) {
+            }
+
+            if (mMode == MODE_PICK_MULTIPLE_PHONES) {
+                pos -= mPhoneNumberAdapter.getCount();
+            }
+
+            if (mFrequentSeparatorPos == ListView.INVALID_POSITION) {
                 // No separator, identity map
                 return pos;
             } else if (pos <= mFrequentSeparatorPos) {
@@ -3446,10 +3625,7 @@
 
         @Override
         public Object getItem(int pos) {
-            if (mSuggestionsCursorCount != 0 && pos <= mSuggestionsCursorCount) {
-                mSuggestionsCursor.moveToPosition(getRealPosition(pos));
-                return mSuggestionsCursor;
-            } else if (isSearchAllContactsItemPosition(pos)){
+            if (isSearchAllContactsItemPosition(pos)){
                 return null;
             } else {
                 int realPosition = getRealPosition(pos);
@@ -3462,13 +3638,7 @@
 
         @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)) {
+            if (isSearchAllContactsItemPosition(pos)) {
                 return 0;
             }
             int realPosition = getRealPosition(pos);
@@ -3565,4 +3735,332 @@
             }
         }
     }
+
+    /**
+     * This class is the adapter for the phone numbers which may not be found in the contacts. It is
+     * called in ContactItemListAdapter in MODE_PICK_MULTIPLE_PHONES mode and shouldn't be a adapter
+     * for any View due to the missing implementation of getItem and getItemId.
+     */
+    private class PhoneNumberAdapter extends BaseAdapter {
+        public static final long INVALID_PHONE_ID = -1;
+
+        /** The initial phone numbers */
+        private List<String> mPhoneNumbers;
+
+        /** The phone numbers after the filtering */
+        private ArrayList<String> mFilteredPhoneNumbers = new ArrayList<String>();
+
+        private Context mContext;
+
+        /** The position where this Adapter Phone numbers start*/
+        private int mStartPos;
+
+        public PhoneNumberAdapter(Context context, final List<String> phoneNumbers) {
+            init(context, phoneNumbers);
+        }
+
+        private void init(Context context, final List<String> phoneNumbers) {
+            mStartPos = (mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0 ? 1 : 0;
+            mContext = context;
+            if (phoneNumbers != null) {
+                mFilteredPhoneNumbers.addAll(phoneNumbers);
+                mPhoneNumbers = phoneNumbers;
+            } else {
+                mPhoneNumbers = new ArrayList<String>();
+            }
+        }
+
+        public int getCount() {
+            int filteredCount = mFilteredPhoneNumbers.size();
+            if (filteredCount == 0) {
+                return 0;
+            }
+            // Count on the separator
+            return 1 + filteredCount;
+        }
+
+        public Object getItem(int position) {
+            // This method is not used currently.
+            throw new RuntimeException("This method is not implemented");
+        }
+
+        public long getItemId(int position) {
+            // This method is not used currently.
+            throw new RuntimeException("This method is not implemented");
+        }
+
+        /**
+         * @return the initial phone numbers, the zero length array is returned when there is no
+         * initial numbers.
+         */
+        public final List<String> getPhoneNumbers() {
+            return mPhoneNumbers;
+        }
+
+        /**
+         * @return the filtered phone numbers, the zero size ArrayList is returned when there is no
+         * initial numbers.
+         */
+        public ArrayList<String> getFilteredPhoneNumbers() {
+            return mFilteredPhoneNumbers;
+        }
+
+        public View getView(int position, View convertView, ViewGroup parent) {
+            int viewCount = getCount();
+            if (viewCount == 0) {
+                return null;
+            }
+            // Separator
+            if (position == mStartPos) {
+                TextView view;
+                if (convertView != null && convertView instanceof TextView) {
+                    view = (TextView) convertView;
+                } else {
+                    LayoutInflater inflater =
+                        (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+                    view = (TextView) inflater.inflate(R.layout.list_separator, parent, false);
+                }
+                view.setText(R.string.unknown_contacts_separator);
+                return view;
+            }
+            // PhoneNumbers start from position of startPos + 1
+            if (position >= mStartPos + 1 && position < mStartPos + viewCount) {
+                View view;
+                if (convertView != null && convertView.getTag() != null &&
+                        convertView.getTag() instanceof ContactListItemCache) {
+                    view = convertView;
+                } else {
+                    view = mAdapter.newView(mContext, null, parent);
+                }
+                bindView(view, mFilteredPhoneNumbers.get(position - 1 - mStartPos));
+                return view;
+            }
+            return null;
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            return position == mStartPos ? IGNORE_ITEM_VIEW_TYPE : super.getItemViewType(position);
+        }
+
+        private void bindView(View view, final String label) {
+            ContactListItemView itemView = (ContactListItemView) view;
+            final ContactListItemCache cache = (ContactListItemCache) view.getTag();
+            itemView.getNameTextView().setText(label);
+            CheckBox checkBox = itemView.getCheckBoxView();
+            checkBox.setChecked(mUserSelection.isSelected(label));
+            itemView.getChipView().setBackgroundResource(0);
+            cache.phoneId = INVALID_PHONE_ID;
+            cache.phoneNumber = label;
+            checkBox.setTag(cache);
+        }
+
+        public void doFilter(final String constraint, boolean selectedOnly) {
+            if (mPhoneNumbers == null) {
+                return;
+            }
+            mFilteredPhoneNumbers.clear();
+            for (String number : mPhoneNumbers) {
+                if (selectedOnly && !mUserSelection.isSelected(number) ||
+                        !TextUtils.isEmpty(constraint) && !number.startsWith(constraint)) {
+                    continue;
+                }
+                mFilteredPhoneNumbers.add(number);
+            }
+        }
+    }
+
+    /**
+     * This class is used to keep the user's selection in MODE_PICK_MULTIPLE_PHONES mode.
+     */
+    private class UserSelection {
+        public static final String EXTRA_SELECTION =
+            "com.android.contacts.ContactsListActivity.UserSelection.extra.SELECTION";
+        private static final String SELECTED_UNKNOWN_PHONES_KEY = "selected_unknown_phones";
+        private static final String SELECTED_PHONE_IDS_KEY = "selected_phone_id";
+
+        /** The PHONE_ID of selected number in user contacts*/
+        private HashSet<Long> mSelectedPhoneIds = new HashSet<Long>();
+
+        /** The selected phone numbers in the PhoneNumberAdapter */
+        private HashSet<String> mSelectedPhoneNumbers = new HashSet<String>();
+
+        /**
+         * @param phoneNumbers the phone numbers are selected.
+         */
+        public UserSelection(final List<String> phoneNumbers, final List<Long> phoneIds) {
+            init(phoneNumbers, phoneIds);
+        }
+
+        /**
+         * Creates from a instance state.
+         */
+        public UserSelection (Bundle icicle) {
+            init(icicle.getStringArray(SELECTED_UNKNOWN_PHONES_KEY),
+                    icicle.getLongArray(SELECTED_PHONE_IDS_KEY));
+        }
+
+        public void saveInstanceState(Bundle icicle) {
+            int selectedUnknownsCount = mSelectedPhoneNumbers.size();
+            if (selectedUnknownsCount > 0) {
+                String[] selectedUnknows = new String[selectedUnknownsCount];
+                icicle.putStringArray(SELECTED_UNKNOWN_PHONES_KEY,
+                        mSelectedPhoneNumbers.toArray(selectedUnknows));
+            }
+            int selectedKnownsCount = mSelectedPhoneIds.size();
+            if (selectedKnownsCount > 0) {
+                long[] selectedPhoneIds = new long [selectedKnownsCount];
+                int index = 0;
+                for (Long phoneId : mSelectedPhoneIds) {
+                    selectedPhoneIds[index++] = phoneId.longValue();
+                }
+                icicle.putLongArray(SELECTED_PHONE_IDS_KEY, selectedPhoneIds);
+
+            }
+        }
+
+        private void init(final String[] selecedUnknownNumbers, final long[] selectedPhoneIds) {
+            if (selecedUnknownNumbers != null) {
+                for (String number : selecedUnknownNumbers) {
+                    setPhoneSelected(number, true);
+                }
+            }
+            if (selectedPhoneIds != null) {
+                for (long id : selectedPhoneIds) {
+                    setPhoneSelected(id, true);
+                }
+            }
+        }
+
+        private void init(final List<String> selecedUnknownNumbers,
+                final List<Long> selectedPhoneIds) {
+            if (selecedUnknownNumbers != null) {
+                setPhoneNumbersSelected(selecedUnknownNumbers, true);
+            }
+            if (selectedPhoneIds != null) {
+                setPhoneIdsSelected(selectedPhoneIds, true);
+            }
+        }
+
+        private void setPhoneNumbersSelected(final List<String> phoneNumbers, boolean selected) {
+            if (selected) {
+                mSelectedPhoneNumbers.addAll(phoneNumbers);
+            } else {
+                mSelectedPhoneNumbers.removeAll(phoneNumbers);
+            }
+        }
+
+        private void setPhoneIdsSelected(final List<Long> phoneIds, boolean selected) {
+            if (selected) {
+                mSelectedPhoneIds.addAll(phoneIds);
+            } else {
+                mSelectedPhoneIds.removeAll(phoneIds);
+            }
+        }
+
+        public void setPhoneSelected(final String phoneNumber, boolean selected) {
+            if (!TextUtils.isEmpty(phoneNumber)) {
+                if (selected) {
+                    mSelectedPhoneNumbers.add(phoneNumber);
+                } else {
+                    mSelectedPhoneNumbers.remove(phoneNumber);
+                }
+            }
+        }
+
+        public void setPhoneSelected(long phoneId, boolean selected) {
+            if (selected) {
+                mSelectedPhoneIds.add(phoneId);
+            } else {
+                mSelectedPhoneIds.remove(phoneId);
+            }
+        }
+
+        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 = mAdapter.getCursor();
+                if (cursor != null) {
+                    int backupPos = cursor.getPosition();
+                    cursor.moveToPosition(-1);
+                    while (cursor.moveToNext()) {
+                        setPhoneSelected(cursor.getLong(PHONE_ID_COLUMN_INDEX), true);
+                    }
+                    cursor.moveToPosition(backupPos);
+                }
+                for (String number : mPhoneNumberAdapter.getFilteredPhoneNumbers()) {
+                    setPhoneSelected(number, true);
+                }
+            } else {
+                mSelectedPhoneIds.clear();
+                mSelectedPhoneNumbers.clear();
+            }
+        }
+
+        public boolean isAllSelected() {
+            return selectedCount() == mPhoneNumberAdapter.getFilteredPhoneNumbers().size()
+                    + mAdapter.getCount();
+        }
+
+        public int selectedCount() {
+            return mSelectedPhoneNumbers.size() + mSelectedPhoneIds.size();
+        }
+
+        public Iterator<Long> getSelectedPhonIds() {
+            return mSelectedPhoneIds.iterator();
+        }
+
+        private int fillSelectedNumbers(Uri[] uris, int from) {
+            int count = mSelectedPhoneNumbers.size();
+            if (count == 0)
+                return from;
+            // Below loop keeps phone numbers by initial order.
+            List<String> phoneNumbers = mPhoneNumberAdapter.getPhoneNumbers();
+            for (String phoneNumber : phoneNumbers) {
+                if (isSelected(phoneNumber)) {
+                    Uri.Builder ub = new Uri.Builder();
+                    ub.scheme(TEL_SCHEME);
+                    ub.encodedOpaquePart(phoneNumber);
+                    uris[from++] = ub.build();
+                }
+            }
+            return from;
+        }
+
+        private int fillSelectedPhoneIds(Uri[] uris, int from) {
+            int count = mSelectedPhoneIds.size();
+            if (count == 0)
+                return from;
+            Iterator<Long> it = mSelectedPhoneIds.iterator();
+            while (it.hasNext()) {
+                uris[from++] = ContentUris.withAppendedId(Phone.CONTENT_URI, it.next());
+            }
+            return from;
+        }
+
+        private Uri[] getSelected() {
+            Uri[] uris = new Uri[mSelectedPhoneNumbers.size() + mSelectedPhoneIds.size()];
+            int from  = fillSelectedNumbers(uris, 0);
+            fillSelectedPhoneIds(uris, from);
+            return uris;
+        }
+
+        public Intent createSelectionIntent() {
+            Intent intent = new Intent();
+            intent.putExtra(Intents.EXTRA_PHONE_URIS, getSelected());
+
+            return intent;
+        }
+
+        public void fillSelectionForSearchMode(Bundle bundle) {
+            bundle.putParcelableArray(EXTRA_SELECTION, getSelected());
+        }
+    }
 }
diff --git a/src/com/android/contacts/ContactsSearchManager.java b/src/com/android/contacts/ContactsSearchManager.java
index d65e079..2297817 100644
--- a/src/com/android/contacts/ContactsSearchManager.java
+++ b/src/com/android/contacts/ContactsSearchManager.java
@@ -40,18 +40,26 @@
     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";
+
+    /**
      * 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, Bundle includedExtras) {
+        context.startActivityForResult(
+                buildIntent(context, initialQuery, includedExtras), requestCode);
     }
 
-    private static Intent buildIntent(Activity context, String initialQuery) {
+    private static Intent buildIntent(
+            Activity context, String initialQuery, Bundle includedExtras) {
         Intent intent = new Intent();
         intent.setData(ContactsContract.Contacts.CONTENT_URI);
         intent.setAction(UI.FILTER_CONTACTS_ACTION);
@@ -64,6 +72,10 @@
         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());
+        intent.putExtra(ORIGINAL_TYPE_EXTRA_KEY, originalIntent.getType());
+        if (includedExtras != null) {
+            intent.putExtras(includedExtras);
+        }
         return intent;
     }
 }
diff --git a/src/com/android/contacts/ImportVCardActivity.java b/src/com/android/contacts/ImportVCardActivity.java
index 0a324fe..2346467 100644
--- a/src/com/android/contacts/ImportVCardActivity.java
+++ b/src/com/android/contacts/ImportVCardActivity.java
@@ -21,32 +21,20 @@
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.app.ProgressDialog;
-import android.content.ContentResolver;
-import android.content.ContentUris;
+import android.app.Service;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
+import android.content.ServiceConnection;
 import android.content.DialogInterface.OnCancelListener;
 import android.content.DialogInterface.OnClickListener;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.Handler;
+import android.os.IBinder;
 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,15 +46,12 @@
 
 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;
 
@@ -105,25 +90,23 @@
  */
 public class ImportVCardActivity extends Activity {
     private static final String LOG_TAG = "ImportVCardActivity";
-    private static final boolean DO_PERFORMANCE_PROFILE = false;
+
+    /* 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 ProgressDialog mProgressDialogForScanVCard;
 
     private List<VCardFile> mAllVCardFileList;
     private VCardScanThread mVCardScanThread;
-    private VCardReadThread mVCardReadThread;
-    private ProgressDialog mProgressDialogForReadVCard;
 
     private String mErrorMessage;
 
-    private boolean mNeedReview = false;
-
     // Runs on the UI thread.
     private class DialogDisplayer implements Runnable {
         private final int mResId;
@@ -152,300 +135,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 +148,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 +181,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 +329,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 +339,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("account_name", mAccountName);
+        intent.putExtra("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,17 +436,14 @@
 
         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("account_name");
+            mAccountType = intent.getStringExtra("account_type");
         } 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) {
+        if (TextUtils.isEmpty(mAccountName) || TextUtils.isEmpty(mAccountType)) {
             // There's three possibilities:
             // - more than one accounts -> ask the user
             // - just one account -> use the account without asking the user
@@ -761,7 +459,9 @@
                     @Override
                     public void onClick(DialogInterface dialog, int which) {
                         dialog.dismiss();
-                        mAccount = mAccountList.get(which);
+                        final Account account = mAccountList.get(which);
+                        mAccountName = account.name;
+                        mAccountType = account.type;
                         // Instead of using Intent mechanism, call the relevant private method,
                         // to avoid throwing an Intent to itself again.
                         startImport();
@@ -770,7 +470,11 @@
                 showDialog(resId);
                 return;
             } else {
-                mAccount = size > 0 ? accountList.get(0) : null;
+                final Account account = ((size > 0) ? accountList.get(0) : null);
+                if (account != null) {
+                    mAccountName = account.name;
+                    mAccountType = account.type;
+                }
             }
         }
 
@@ -782,13 +486,9 @@
         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;
-        }
 
         if (uri != null) {
-            importOneVCardFromSDCard(uri);
+            importVCard(uri.toString());
         } else {
             doScanExternalStorageAndImportVCard();
         }
@@ -845,19 +545,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)));
@@ -891,11 +578,6 @@
     @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 +594,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 +614,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..aea47cb
--- /dev/null
+++ b/src/com/android/contacts/ImportVCardService.java
@@ -0,0 +1,462 @@
+/*
+ * 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 final int mPreferedVCardType;
+        private VCardParser mVCardParser;
+        private boolean mCanceled;
+        private final List<Uri> mErrorUris;
+        private final List<Uri> mCreatedUris;
+
+        public VCardReadThread() {
+            mContext = ImportVCardService.this;
+            mResolver = mContext.getContentResolver();
+            mPreferedVCardType = VCardConfig.getVCardTypeFromString(
+                    mContext.getString(R.string.config_import_vcard_type));
+            mErrorUris = new ArrayList<Uri>();
+            mCreatedUris = new ArrayList<Uri>();
+        }
+
+        @Override
+        public void run() {
+            while (!mCanceled) {
+                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);
+                mErrorUris.clear();
+                mCreatedUris.clear();
+            }
+            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>();
+            final String defaultCharset = VCardConfig.DEFAULT_IMPORT_CHARSET;
+            // 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));
+                try {
+                    if (!readOneVCard(uri, defaultCharset,
+                            interpreterCollection, null, true)) {
+                        mErrorUris.add(uri);
+                    }
+                } catch (VCardNestedException e) {
+                    // Assume that VCardSourceDetector was able to detect the source.
+                }
+
+                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 = mPreferedVCardType;
+                String charset = detector.getEstimatedCharset();
+                if (charset == null) {
+                    charset = defaultCharset;
+                }
+                final VCardEntryConstructor constructor =
+                        new VCardEntryConstructor(charset, charset, false, vcardType, account);
+                final VCardEntryCommitter committer = new VCardEntryCommitter(mResolver);
+                final ProgressNotifier notifier = new ProgressNotifier(id);
+                constructor.addEntryHandler(committer);
+                constructor.addEntryHandler(notifier);
+
+                try {
+                    if (!readOneVCard(uri, charset, constructor, detector, false)) {
+                        Log.e(LOG_TAG, "Failed to read \"" + uri.toString() + "\" " +
+                                "while first scan was successful.");
+                    }
+                } catch (VCardNestedException e) {
+                    // We should already know the number of nests in the first scan and
+                    // treat them at this time.
+                    Log.e(LOG_TAG, "Must not reach here.");
+                }
+                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, String charset, VCardInterpreter interpreter,
+                VCardSourceDetector detector, boolean throwNestedException)
+                throws VCardNestedException {
+            InputStream is;
+            try {
+                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(detector);
+                    if (mCanceled) {
+                        mVCardParser.cancel();
+                    }
+                }
+
+                try {
+                    mVCardParser.parse(is, charset, 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();
+                        if (mCanceled) {
+                            mVCardParser.cancel();
+                        }
+                    }
+
+                    try {
+                        mVCardParser.parse(is, charset, 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 (VCardNotSupportedException e) {
+                if ((e instanceof VCardNestedException) && throwNestedException) {
+                    throw (VCardNestedException) 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..47c7547
--- /dev/null
+++ b/src/com/android/contacts/JoinContactActivity.java
@@ -0,0 +1,382 @@
+/*
+ * 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 android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+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.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+/**
+ * An activity that shows a list of contacts that can be joined with the target contact.
+ */
+public class JoinContactActivity extends ContactsListActivity {
+
+    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";
+
+    /** Maximum number of suggestions shown for joining aggregates */
+    private static final int MAX_SUGGESTIONS = 4;
+
+    private long mTargetContactId;
+
+    /**
+     * 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;
+
+    private boolean mLoadingJoinSuggestions;
+
+    private JoinContactListAdapter mAdapter;
+
+    @Override
+    protected void resolveIntent(Intent intent) {
+        mMode = MODE_PICK_CONTACT;
+        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();
+        }
+    }
+
+    @Override
+    public void initContentView() {
+        setContentView(R.layout.contacts_list_content_join);
+        TextView blurbView = (TextView)findViewById(R.id.join_contact_blurb);
+
+        String blurb = getString(R.string.blurbJoinContactDataWith,
+                getContactDisplayName(mTargetContactId));
+        blurbView.setText(blurb);
+        mJoinModeShowAllContacts = true;
+        mAdapter = new JoinContactListAdapter(this);
+        setupListView(mAdapter);
+    }
+
+    @Override
+    protected void onListItemClick(int position, long id) {
+        if (id == JOIN_MODE_SHOW_ALL_CONTACTS_ID) {
+            mJoinModeShowAllContacts = false;
+            startQuery();
+        } else {
+            final Uri uri = getSelectedUri(position);
+            returnPickerResult(null, null, uri);
+        }
+    }
+
+    @Override
+    protected Uri getUriToQuery() {
+        return getJoinSuggestionsUri(null);
+    }
+
+    /*
+     * TODO: move to a background thread.
+     */
+    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);
+            }
+        } finally {
+            if (c != null) {
+                c.close();
+            }
+        }
+
+        if (contactName == null) {
+            contactName = "";
+        }
+
+        return contactName;
+    }
+
+    private Uri getJoinSuggestionsUri(String filter) {
+        Builder builder = Contacts.CONTENT_URI.buildUpon();
+        builder.appendEncodedPath(String.valueOf(mTargetContactId));
+        builder.appendEncodedPath(AggregationSuggestions.CONTENT_DIRECTORY);
+        if (!TextUtils.isEmpty(filter)) {
+            builder.appendEncodedPath(Uri.encode(filter));
+        }
+        builder.appendQueryParameter("limit", String.valueOf(MAX_SUGGESTIONS));
+        return builder.build();
+    }
+
+    @Override
+    Cursor doFilter(String filter) {
+        throw new UnsupportedOperationException();
+    }
+
+    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;
+    }
+
+    @Override
+    protected void startQuery(Uri uri, String[] projection) {
+        mLoadingJoinSuggestions = true;
+        startQuery(uri, projection, null, null, null);
+    }
+
+    @Override
+    protected void onQueryComplete(Cursor cursor) {
+        // 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) {
+                mAdapter.setSuggestionsCursor(cursor);
+            } else {
+                cursor.close();
+                mAdapter.setSuggestionsCursor(null);
+            }
+
+            if (mAdapter.mSuggestionsCursorCount == 0
+                    || !mJoinModeShowAllContacts) {
+                startQuery(getContactFilterUri(getTextFilter()),
+                        CONTACTS_SUMMARY_PROJECTION,
+                        Contacts._ID + " != " + mTargetContactId
+                                + " AND " + ContactsContract.Contacts.IN_VISIBLE_GROUP + "=1", null,
+                        getSortOrder(CONTACTS_SUMMARY_PROJECTION));
+                return;
+            }
+
+            cursor = getShowAllContactsLabelCursor(CONTACTS_SUMMARY_PROJECTION);
+        }
+
+        super.onQueryComplete(cursor);
+    }
+
+    private class JoinContactListAdapter extends ContactItemListAdapter {
+        Cursor mSuggestionsCursor;
+        int mSuggestionsCursorCount;
+
+        public JoinContactListAdapter(Context context) {
+            super(context);
+        }
+
+        public void setSuggestionsCursor(Cursor cursor) {
+            if (mSuggestionsCursor != null) {
+                mSuggestionsCursor.close();
+            }
+            mSuggestionsCursor = cursor;
+            mSuggestionsCursorCount = cursor == null ? 0 : cursor.getCount();
+        }
+
+        private boolean isShowAllContactsItemPosition(int position) {
+            return mJoinModeShowAllContacts
+                    && mSuggestionsCursorCount != 0 && 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");
+            }
+
+            if (isShowAllContactsItemPosition(position)) {
+                return getLayoutInflater().
+                        inflate(R.layout.contacts_list_show_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, !showingSuggestion);
+            return v;
+        }
+
+        @Override
+        public void changeCursor(Cursor cursor) {
+            if (cursor == null) {
+                mAdapter.setSuggestionsCursor(null);
+            }
+
+            super.changeCursor(cursor);
+        }
+        @Override
+        public int getItemViewType(int position) {
+            if (isShowAllContactsItemPosition(position)) {
+                return IGNORE_ITEM_VIEW_TYPE;
+            }
+
+            return super.getItemViewType(position);
+        }
+
+        private int getSeparatorId(int position) {
+            if (mSuggestionsCursorCount != 0) {
+                if (position == 0) {
+                    return R.string.separatorJoinAggregateSuggestions;
+                } else if (position == mSuggestionsCursorCount + 1) {
+                    return R.string.separatorJoinAggregateAll;
+                }
+            }
+            return 0;
+        }
+
+        @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 (mSuggestionsCursorCount != 0) {
+                // When showing suggestions, we have 2 additional list items: the "Suggestions"
+                // and "All contacts" headers.
+                return mSuggestionsCursorCount + superCount + 2;
+            }
+            return superCount;
+        }
+
+        private int getRealPosition(int pos) {
+            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 {
+                // No separator, identity map
+                return pos;
+            }
+        }
+
+        @Override
+        public Object getItem(int pos) {
+            if (mSuggestionsCursorCount != 0 && pos <= mSuggestionsCursorCount) {
+                mSuggestionsCursor.moveToPosition(getRealPosition(pos));
+                return mSuggestionsCursor;
+            } 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;
+                }
+            }
+            int realPosition = getRealPosition(pos);
+            if (realPosition < 0) {
+                return 0;
+            }
+            return super.getItemId(realPosition);
+        }
+    }
+}
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/ViewContactActivity.java b/src/com/android/contacts/ViewContactActivity.java
index ead6a4a..c15a40d 100644
--- a/src/com/android/contacts/ViewContactActivity.java
+++ b/src/com/android/contacts/ViewContactActivity.java
@@ -685,11 +685,8 @@
             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);
-            }
+            Intent intent = new Intent(JoinContactActivity.JOIN_CONTACT);
+            intent.putExtra(JoinContactActivity.EXTRA_TARGET_CONTACT_ID, freshId);
             startActivityForResult(intent, REQUEST_JOIN_CONTACT);
         }
     }
diff --git a/src/com/android/contacts/activities/ContactDetailActivity.java b/src/com/android/contacts/activities/ContactDetailActivity.java
new file mode 100644
index 0000000..dc14770
--- /dev/null
+++ b/src/com/android/contacts/activities/ContactDetailActivity.java
@@ -0,0 +1,145 @@
+/*
+ * 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.mvcframework.DialogManager;
+import com.android.contacts.mvcframework.LoaderActivity;
+import com.android.contacts.views.detail.ContactDetailView;
+import com.android.contacts.views.detail.ContactLoader;
+
+import android.app.Dialog;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+
+public class ContactDetailActivity extends LoaderActivity<ContactLoader.Result> implements
+        DialogManager.DialogShowingViewActivity {
+    private static final int LOADER_DETAILS = 1;
+    private ContactDetailView mDetails;
+    private DialogManager mDialogManager;
+
+    private static final String TAG = "ContactDetailActivity";
+
+    private static final int DIALOG_VIEW_DIALOGS_ID1 = 1;
+    private static final int DIALOG_VIEW_DIALOGS_ID2 = 2;
+
+    @Override
+    public void onCreate(Bundle savedState) {
+        super.onCreate(savedState);
+
+        setContentView(R.layout.contact_detail);
+
+        mDialogManager = new DialogManager(this, DIALOG_VIEW_DIALOGS_ID1, DIALOG_VIEW_DIALOGS_ID2);
+
+        mDetails = (ContactDetailView) findViewById(R.id.contact_details);
+        mDetails.setCallbacks(new ContactDetailView.DefaultCallbacks(this));
+    }
+
+    @Override
+    public void onInitializeLoaders() {
+        startLoading(LOADER_DETAILS, null);
+    }
+
+    @Override
+    protected ContactLoader onCreateLoader(int id, Bundle args) {
+        switch (id) {
+            case LOADER_DETAILS: {
+                return new ContactLoader(this, getIntent().getData());
+            }
+        }
+        return null;
+    }
+
+
+    @Override
+    public void onLoadComplete(int id, ContactLoader.Result data) {
+        switch (id) {
+            case LOADER_DETAILS:
+                if (data == ContactLoader.Result.NOT_FOUND) {
+                    // Item has been deleted
+                    Log.i(TAG, "No contact found. Closing activity");
+                    finish();
+                    return;
+                }
+                mDetails.setData(data);
+                break;
+        }
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        // TODO: This is too hardwired.
+        if (mDetails.onCreateOptionsMenu(menu, getMenuInflater())) return true;
+
+        return super.onCreateOptionsMenu(menu);
+    }
+
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        // TODO: This is too hardwired.
+        if (mDetails.onPrepareOptionsMenu(menu)) return true;
+
+        return super.onPrepareOptionsMenu(menu);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        // TODO: This is too hardwired.
+        if (mDetails.onOptionsItemSelected(item)) return true;
+
+        return super.onOptionsItemSelected(item);
+    }
+
+    public DialogManager getDialogManager() {
+        return mDialogManager;
+    }
+
+    @Override
+    protected Dialog onCreateDialog(int id, Bundle args) {
+        return mDialogManager.onCreateDialog(id, args);
+    }
+
+    @Override
+    public boolean onContextItemSelected(MenuItem item) {
+        // TODO: This is too hardwired.
+        if (mDetails.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 (mDetails.onKeyDown(keyCode, event)) return true;
+
+        return super.onKeyDown(keyCode, event);
+    }
+}
diff --git a/src/com/android/contacts/mvcframework/CursorLoader.java b/src/com/android/contacts/mvcframework/CursorLoader.java
new file mode 100644
index 0000000..25585be
--- /dev/null
+++ b/src/com/android/contacts/mvcframework/CursorLoader.java
@@ -0,0 +1,105 @@
+/*
+ * 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.mvcframework;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.os.AsyncTask;
+
+public abstract class CursorLoader extends Loader<Cursor> {
+    Cursor mCursor;
+    ForceLoadContentObserver mObserver;
+    boolean mClosed;
+
+    final class LoadListTask extends AsyncTask<Void, Void, Cursor> {
+        /* Runs on a worker thread */
+        @Override
+        protected Cursor doInBackground(Void... params) {
+            Cursor cursor = doQueryInBackground();
+            // Ensure the data is loaded
+            if (cursor != null) {
+                cursor.getCount();
+                cursor.registerContentObserver(mObserver);
+            }
+            return cursor;
+        }
+
+        /* Runs on the UI thread */
+        @Override
+        protected void onPostExecute(Cursor cursor) {
+            if (mClosed) {
+                // An async query came in after the call to close()
+                cursor.close();
+                return;
+            }
+            mCursor = cursor;
+            deliverResult(cursor);
+        }
+    }
+
+    public CursorLoader(Context context) {
+        super(context);
+        mObserver = new ForceLoadContentObserver();
+    }
+
+    /**
+     * 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() {
+        if (mCursor != null) {
+            deliverResult(mCursor);
+        } else {
+            forceLoad();
+        }
+    }
+
+    /**
+     * Force an asynchronous load. Unlike {@link #startLoading()} this will ignore a previously
+     * loaded data set and load a new one.
+     */
+    @Override
+    public void forceLoad() {
+        new LoadListTask().execute((Void[]) null);
+    }
+
+    /**
+     * Must be called from the UI thread
+     */
+    @Override
+    public void stopLoading() {
+        if (mCursor != null && !mCursor.isClosed()) {
+            mCursor.close();
+            mCursor = null;
+        }
+    }
+
+    @Override
+    public void destroy() {
+        // Close up the cursor
+        stopLoading();
+        // Make sure that any outstanding loads clean themselves up properly
+        mClosed = true;
+    }
+
+    /** Called from a worker thread to execute the desired query */
+    protected abstract Cursor doQueryInBackground();
+}
diff --git a/src/com/android/contacts/mvcframework/DialogManager.java b/src/com/android/contacts/mvcframework/DialogManager.java
new file mode 100644
index 0000000..420c4b2
--- /dev/null
+++ b/src/com/android/contacts/mvcframework/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.mvcframework;
+
+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/mvcframework/Loader.java b/src/com/android/contacts/mvcframework/Loader.java
new file mode 100644
index 0000000..fab2f46
--- /dev/null
+++ b/src/com/android/contacts/mvcframework/Loader.java
@@ -0,0 +1,119 @@
+/*
+ * 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.mvcframework;
+
+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> {
+        public void onLoadComplete(int id, D data);
+    }
+
+    protected void deliverResult(D data) {
+        if (mListener != null) {
+            mListener.onLoadComplete(mId, data);
+
+        }
+    }
+
+    public Loader(Context context) {
+        mContext = context.getApplicationContext();
+    }
+
+    /**
+     * @return an application context
+     */
+    public Context getContext() {
+        return mContext;
+    }
+
+    /**
+     * 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.
+     */
+    public abstract void stopLoading();
+
+    /**
+     * Must be called from the UI thread
+     */
+    public abstract void destroy();
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/mvcframework/LoaderActivity.java b/src/com/android/contacts/mvcframework/LoaderActivity.java
new file mode 100644
index 0000000..0e74d62
--- /dev/null
+++ b/src/com/android/contacts/mvcframework/LoaderActivity.java
@@ -0,0 +1,144 @@
+/*
+ * 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.mvcframework;
+
+
+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> {
+    static final class LoaderInfo {
+        public Bundle args;
+        public Loader loader;
+    }
+    private HashMap<Integer, LoaderInfo> mLoaders;
+
+    /**
+     * Registers a loader with this activity, registers the callbacks on it, and starts it loading.
+     */
+    protected void startLoading(int id, Bundle args) {
+        LoaderInfo info = mLoaders.get(id);
+        Loader loader;
+        if (info != null) {
+            loader = info.loader;
+            if (loader != null) {
+                loader.unregisterListener(this);
+                loader.destroy();
+                info.loader = null;
+            }
+        } else {
+            info = new LoaderInfo();
+            info.args = args;
+        }
+        mLoaders.put(id, info);
+        loader = onCreateLoader(id, args);
+        loader.registerListener(id, this);
+        loader.startLoading();
+        info.loader = loader;
+    }
+
+    protected abstract Loader onCreateLoader(int id, Bundle args);
+    protected abstract void onInitializeLoaders();
+
+    public abstract void onLoadComplete(int id, D data);
+
+    @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>) getLastNonConfigurationInstance();
+            if (mLoaders == null) {
+                mLoaders = new HashMap<Integer, LoaderInfo>();
+                onInitializeLoaders();
+            }
+        }
+    }
+
+    @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> entry : mLoaders.entrySet()) {
+            LoaderInfo info = entry.getValue();
+            Loader loader = info.loader;
+            int id = entry.getKey();
+            if (loader == null) {
+               loader = onCreateLoader(id, info.args);
+               info.loader = loader;
+            } else {
+                loader.registerListener(id, this);
+            }
+            loader.startLoading();
+        }
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+
+        for (HashMap.Entry<Integer, LoaderInfo> entry : mLoaders.entrySet()) {
+            LoaderInfo info = entry.getValue();
+            Loader 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();
+//            }
+        }
+    }
+
+    @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> entry : mLoaders.entrySet()) {
+                LoaderInfo info = entry.getValue();
+                Loader loader = info.loader;
+                if (loader == null) {
+                    continue;
+                }
+                loader.destroy();
+            }
+        }
+    }
+}
diff --git a/src/com/android/contacts/ui/EditContactActivity.java b/src/com/android/contacts/ui/EditContactActivity.java
index c70cff6..993417d 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;
@@ -30,11 +30,11 @@
 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.mvcframework.DialogManager;
 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 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;
     }
 
     /**
@@ -1409,4 +1413,8 @@
             ContactsSearchManager.startSearch(this, initialQuery);
         }
     }
+
+    public DialogManager getDialogManager() {
+        return mDialogManager;
+    }
 }
diff --git a/src/com/android/contacts/ui/widget/GenericEditorView.java b/src/com/android/contacts/ui/widget/GenericEditorView.java
index 24262bb..30ef8c1 100644
--- a/src/com/android/contacts/ui/widget/GenericEditorView.java
+++ b/src/com/android/contacts/ui/widget/GenericEditorView.java
@@ -25,6 +25,8 @@
 import com.android.contacts.model.ContactsSource.EditField;
 import com.android.contacts.model.ContactsSource.EditType;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.mvcframework.DialogManager;
+import com.android.contacts.mvcframework.DialogManager.DialogShowingView;
 import com.android.contacts.ui.ViewIdGenerator;
 
 import android.app.AlertDialog;
@@ -32,6 +34,7 @@
 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;
@@ -58,10 +61,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
@@ -85,6 +93,7 @@
     private EditType mPendingType;
 
     private ViewIdGenerator mViewIdGenerator;
+    private DialogManager mDialogManager = null;
 
     public GenericEditorView(Context context) {
         super(context);
@@ -354,7 +363,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,7 +385,7 @@
     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: {
@@ -402,6 +411,26 @@
         }
     }
 
+    /* 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 +498,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/views/detail/ContactDetailView.java b/src/com/android/contacts/views/detail/ContactDetailView.java
new file mode 100644
index 0000000..424c63a
--- /dev/null
+++ b/src/com/android/contacts/views/detail/ContactDetailView.java
@@ -0,0 +1,1022 @@
+/*
+ * 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.mvcframework.DialogManager;
+import com.android.contacts.util.Constants;
+import com.android.contacts.util.DataStatus;
+import com.android.contacts.views.detail.ContactLoader.Result;
+import com.android.internal.telephony.ITelephony;
+import com.android.internal.widget.ContactHeaderWidget;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+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.AttributeSet;
+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.OnCreateContextMenuListener;
+import android.widget.AdapterView;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.AdapterView.OnItemClickListener;
+
+import java.util.ArrayList;
+
+public class ContactDetailView extends LinearLayout implements OnCreateContextMenuListener,
+        OnItemClickListener, DialogManager.DialogShowingView {
+    private static final String TAG = "ContactDetailsView";
+    private static final boolean SHOW_SEPARATORS = false;
+
+    private static final String DIALOG_ID_KEY = "dialog_id";
+    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 MENU_ITEM_MAKE_DEFAULT = 3;
+
+    private Result mContactData;
+    private Callbacks mCallbacks;
+    private LayoutInflater mInflater;
+    private ContactHeaderWidget mContactHeaderWidget;
+    private ListView mListView;
+    private boolean mShowSmsLinksForAllPhones;
+    private ViewAdapter mAdapter;
+    private Uri mPrimaryPhoneUri = null;
+    private DialogManager mDialogManager = null;
+
+    private int mReadOnlySourcesCnt;
+    private int mWritableSourcesCnt;
+    private boolean mAllRestricted;
+    private 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 ContactDetailView(Context context) {
+        super(context);
+    }
+
+    public ContactDetailView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public void setData(Result data) {
+        mContactData = data;
+
+        mContactHeaderWidget.bindFromContactLookupUri(data.getUri());
+        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(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);
+                    }
+                }
+            }
+        }
+    }
+
+    public interface Callbacks {
+        public void onPrimaryClick(ViewEntry entry);
+        public void onSecondaryClick(ViewEntry entry);
+    }
+
+    public static final class DefaultCallbacks implements Callbacks {
+        private Context mContext;
+
+        public DefaultCallbacks(Context context) {
+            mContext = context;
+        }
+
+        public void onPrimaryClick(ViewEntry entry) {
+            Intent intent = entry.intent;
+            if (intent != null) {
+                try {
+                    mContext.startActivity(intent);
+                } catch (ActivityNotFoundException e) {
+                    Log.e(TAG, "No activity found for intent: " + intent);
+                }
+            }
+        }
+
+        public void onSecondaryClick(ViewEntry entry) {
+            Intent intent = entry.secondaryIntent;
+            if (intent != null) {
+                try {
+                    mContext.startActivity(intent);
+                } catch (ActivityNotFoundException e) {
+                    Log.e(TAG, "No activity found for intent: " + intent);
+                }
+            }
+        }
+    }
+
+    public void setCallbacks(Callbacks callbacks) {
+        mCallbacks = callbacks;
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        Context context = getContext();
+        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        mContactHeaderWidget = (ContactHeaderWidget) findViewById(R.id.contact_header_widget);
+        mContactHeaderWidget.showStar(true);
+        mContactHeaderWidget.setExcludeMimes(new String[] {
+            Contacts.CONTENT_ITEM_TYPE
+        });
+
+        mListView = (ListView) 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 = findViewById(android.R.id.empty);
+
+        // 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;
+    }
+
+    /* 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;
+            mCallbacks.onSecondaryClick(entry);
+        }
+    }
+
+    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) {
+                    long rawContactIdToEdit = mRawContactIds.get(0);
+                    Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
+                            rawContactIdToEdit);
+                    mContext.startActivity(new Intent(Intent.ACTION_EDIT, 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) {
+            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);
+        }
+    }
+
+    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;
+        mCallbacks.onPrimaryClick(entry);
+    }
+
+    private final DialogInterface.OnClickListener mDeleteListener =
+            new DialogInterface.OnClickListener() {
+        public void onClick(DialogInterface dialog, int which) {
+            mContext.getContentResolver().delete(mContactData.getLookupUri(), null, null);
+        }
+    };
+
+    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_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 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 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 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:
+                throw new IllegalArgumentException("Invalid dialogId: " + dialogId);
+        }
+    }
+
+    /* 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;
+    }
+
+    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);
+    }
+
+    @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) {
+                    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 super.onKeyDown(keyCode, event);
+    }
+}
diff --git a/src/com/android/contacts/views/detail/ContactLoader.java b/src/com/android/contacts/views/detail/ContactLoader.java
new file mode 100644
index 0000000..103136a
--- /dev/null
+++ b/src/com/android/contacts/views/detail/ContactLoader.java
@@ -0,0 +1,390 @@
+/*
+ * 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.mvcframework.Loader;
+import com.android.contacts.util.DataStatus;
+
+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 ContactLoader extends Loader<ContactLoader.Result> {
+    private Uri mLookupUri;
+    private Result mContact;
+    private ForceLoadContentObserver mObserver;
+    private boolean mDestroyed;
+
+    private static final String TAG = "ContactLoader";
+
+    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();
+
+        private final Uri mLookupUri;
+        private final String mLookupKey;
+        private final Uri mUri;
+        private final long mId;
+        private final ArrayList<Entity> mEntities;
+        private final HashMap<Long, DataStatus> mStatuses;
+        private final long mNameRawContactId;
+        private final int mDisplayNameSource;
+
+        /**
+         * 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;
+        }
+
+        /**
+         * Constructor to call when contact was found
+         */
+        private Result(Uri lookupUri, String lookupKey, Uri uri, long id, long nameRawContactId,
+                int displayNameSource) {
+            mLookupUri = lookupUri;
+            mLookupKey = lookupKey;
+            mUri = uri;
+            mId = id;
+            mEntities = new ArrayList<Entity>();
+            mStatuses = new HashMap<Long, DataStatus>();
+            mNameRawContactId = nameRawContactId;
+            mDisplayNameSource = displayNameSource;
+        }
+
+        public Uri getLookupUri() {
+            return mLookupUri;
+        }
+        public String getLookupKey() {
+            return mLookupKey;
+        }
+        public Uri getUri() {
+            return mUri;
+        }
+        public long getId() {
+            return mId;
+        }
+        public ArrayList<Entity> getEntities() {
+            return mEntities;
+        }
+        public HashMap<Long, DataStatus> getStatuses() {
+            return mStatuses;
+        }
+        public long getNameRawContactId() {
+            return mNameRawContactId;
+        }
+        public int getDisplayNameSource() {
+            return mDisplayNameSource;
+        }
+    }
+
+    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;
+    }
+
+    public final class LoadContactTask extends AsyncTask<Void, Void, Result> {
+
+        /**
+         * Used for synchronuous calls in unit test
+         * @hide
+         */
+        public Result testExecute() {
+            return doInBackground();
+        }
+
+        @Override
+        protected Result doInBackground(Void... args) {
+            final ContentResolver resolver = getContext().getContentResolver();
+            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;
+        }
+
+        /**
+         * 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 Uri dataUri = Uri.withAppendedPath(contactUri, Contacts.Data.CONTENT_DIRECTORY);
+
+            final Cursor cursor = resolver.query(dataUri,
+                    new String[] {
+                        Contacts.NAME_RAW_CONTACT_ID,
+                        Contacts.DISPLAY_NAME_SOURCE,
+                        Contacts.LOOKUP_KEY
+                    }, 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;
+                }
+                String lookupKey =
+                        cursor.getString(cursor.getColumnIndex(Contacts.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;
+                }
+
+                long nameRawContactId = cursor.getLong(cursor.getColumnIndex(
+                        Contacts.NAME_RAW_CONTACT_ID));
+                int displayNameSource = cursor.getInt(cursor.getColumnIndex(
+                        Contacts.DISPLAY_NAME_SOURCE));
+
+                return new Result(lookupUri, lookupKey, contactUri, uriContactId, nameRawContactId,
+                        displayNameSource);
+            } 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);
+                getContext().getContentResolver().registerContentObserver(
+                        mLookupUri, true, mObserver);
+                deliverResult(result);
+            }
+        }
+    }
+
+    public ContactLoader(Context context, Uri lookupUri) {
+            super(context);
+        mLookupUri = lookupUri;
+    }
+
+    @Override
+    public void startLoading() {
+        if (mContact != null) {
+            deliverResult(mContact);
+        } else {
+            forceLoad();
+        }
+    }
+
+    @Override
+    public void forceLoad() {
+        new LoadContactTask().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/tests/src/com/android/contacts/ContactDetailLoaderTest.java b/tests/src/com/android/contacts/ContactDetailLoaderTest.java
new file mode 100644
index 0000000..f7a7a84
--- /dev/null
+++ b/tests/src/com/android/contacts/ContactDetailLoaderTest.java
@@ -0,0 +1,296 @@
+/*
+ * 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.views.detail.ContactLoader;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.test.AndroidTestCase;
+
+import junit.framework.AssertionFailedError;
+
+/**
+ * Runs ContactLoader tests for the the contact-detail view.
+ * TODO: Warning: This currently only works on wiped phones as this will wipe
+ * your contact data
+ * TODO: Test all fields returned by the Loader
+ * TODO: Test social entries returned by the Loader
+ */
+public class ContactDetailLoaderTest extends AndroidTestCase {
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        //mContext.getContentResolver().delete(Data.CONTENT_URI, null, null);
+        //mContext.getContentResolver().delete(RawContacts.CONTENT_URI, null, null);
+    }
+
+    /**
+     * 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");
+    }
+
+    private ContactLoader.Result assertLoadContact(Uri uri) {
+        final ContactLoader loader = new ContactLoader(mContext, uri);
+        final ContactLoader.LoadContactTask loadContactTask = loader.new LoadContactTask();
+        return loadContactTask.testExecute();
+    }
+
+    protected Uri insertStructuredName(long rawContactId, String givenName, String familyName) {
+        ContentValues values = new ContentValues();
+        StringBuilder sb = new StringBuilder();
+        if (givenName != null) {
+            sb.append(givenName);
+        }
+        if (givenName != null && familyName != null) {
+            sb.append(" ");
+        }
+        if (familyName != null) {
+            sb.append(familyName);
+        }
+        values.put(StructuredName.DISPLAY_NAME, sb.toString());
+        values.put(StructuredName.GIVEN_NAME, givenName);
+        values.put(StructuredName.FAMILY_NAME, familyName);
+
+        return insertStructuredName(rawContactId, values);
+    }
+
+    protected Uri insertStructuredName(long rawContactId, ContentValues values) {
+        values.put(Data.RAW_CONTACT_ID, rawContactId);
+        values.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+        Uri resultUri = getContext().getContentResolver().insert(Data.CONTENT_URI, values);
+        return resultUri;
+    }
+
+    protected Cursor queryRawContact(long rawContactId) {
+        return getContext().getContentResolver().query(ContentUris.withAppendedId(
+                RawContacts.CONTENT_URI, rawContactId), null, null, null, null);
+    }
+
+    protected Cursor queryContact(long contactId) {
+        return getContext().getContentResolver().query(ContentUris.withAppendedId(
+                Contacts.CONTENT_URI, contactId), null, null, null, null);
+    }
+
+    private long getContactIdByRawContactId(long rawContactId) {
+        Cursor c = queryRawContact(rawContactId);
+        assertTrue(c.moveToFirst());
+        long contactId = c.getLong(c.getColumnIndex(RawContacts.CONTACT_ID));
+        c.close();
+        return contactId;
+    }
+
+    private String getContactLookupByContactId(long contactId) {
+        Cursor c = queryContact(contactId);
+        assertTrue(c.moveToFirst());
+        String lookup = c.getString(c.getColumnIndex(Contacts.LOOKUP_KEY));
+        c.close();
+        return lookup;
+    }
+
+    public long createRawContact(String sourceId, String givenName, String familyName) {
+        ContentValues values = new ContentValues();
+
+        values.put(RawContacts.ACCOUNT_NAME, "aa");
+        values.put(RawContacts.ACCOUNT_TYPE, "mock");
+        values.put(RawContacts.SOURCE_ID, sourceId);
+        values.put(RawContacts.VERSION, 1);
+        values.put(RawContacts.DELETED, 0);
+        values.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT);
+        values.put(RawContacts.CUSTOM_RINGTONE, "d");
+        values.put(RawContacts.SEND_TO_VOICEMAIL, 1);
+        values.put(RawContacts.LAST_TIME_CONTACTED, 12345);
+        values.put(RawContacts.STARRED, 1);
+        values.put(RawContacts.SYNC1, "e");
+        values.put(RawContacts.SYNC2, "f");
+        values.put(RawContacts.SYNC3, "g");
+        values.put(RawContacts.SYNC4, "h");
+
+        Uri rawContactUri =
+            getContext().getContentResolver().insert(RawContacts.CONTENT_URI, values);
+
+        long rawContactId = ContentUris.parseId(rawContactUri);
+        insertStructuredName(rawContactId, givenName, familyName);
+        return rawContactId;
+    }
+
+    public void testNullUri() {
+        IllegalArgumentException e =
+            assertThrows(IllegalArgumentException.class, new Runnable() {
+                public void run() {
+                    assertLoadContact(null);
+                }
+            });
+        assertEquals(e.getMessage(), "uri must not be null");
+    }
+
+    public void testEmptyUri() {
+        IllegalArgumentException e =
+            assertThrows(IllegalArgumentException.class, new Runnable() {
+                public void run() {
+                    assertLoadContact(Uri.EMPTY);
+                }
+            });
+        assertEquals(e.getMessage(), "uri format is unknown");
+    }
+
+    public void testInvalidUri() {
+        IllegalArgumentException e =
+                assertThrows(IllegalArgumentException.class, new Runnable() {
+                    public void run() {
+                        assertLoadContact(Uri.parse("content://wtf"));
+                    }
+                });
+        assertEquals(e.getMessage(), "uri format is unknown");
+    }
+
+    public void testLoadContactWithContactIdUri() {
+        // Use content Uris that only contain the ID
+        // Use some special characters in the source id to ensure that Encode/Decode properly
+        // works in Uris
+        long rawContactId1 = createRawContact("JohnDoe:;\"'[]{}=+-_\\|/.,<>?!@#$", "John", "Doe");
+        long rawContactId2 = createRawContact("JaneDuh%12%%^&*()", "Jane", "Duh");
+
+        long contactId1 = getContactIdByRawContactId(rawContactId1);
+        long contactId2 = getContactIdByRawContactId(rawContactId2);
+
+        Uri contactUri1 = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId1);
+        Uri contactUri2 = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId2);
+
+        ContactLoader.Result contact1 = assertLoadContact(contactUri1);
+        ContactLoader.Result contact2 = assertLoadContact(contactUri2);
+
+        assertEquals(contactId1, contact1.getId());
+        assertEquals(contactId2, contact2.getId());
+    }
+
+    public void testLoadContactWithOldStyleUri() {
+        // Use content Uris that only contain the ID but use the format used in Donut
+        long rawContactId1 = createRawContact("JohnDoe", "John", "Doe");
+        long rawContactId2 = createRawContact("JaneDuh", "Jane", "Duh");
+
+        Uri oldUri1 = ContentUris.withAppendedId(Uri.parse("content://contacts"), rawContactId1);
+        Uri oldUri2 = ContentUris.withAppendedId(Uri.parse("content://contacts"), rawContactId2);
+
+        ContactLoader.Result contact1 = assertLoadContact(oldUri1);
+        ContactLoader.Result contact2 = assertLoadContact(oldUri2);
+
+        long contactId1 = getContactIdByRawContactId(rawContactId1);
+        long contactId2 = getContactIdByRawContactId(rawContactId2);
+
+        assertEquals(contactId1, contact1.getId());
+        assertEquals(contactId2, contact2.getId());
+    }
+
+    public void testLoadContactWithContactLookupUri() {
+        // Use lookup-style Uris that do not contain the Contact-ID
+        long rawContactId1 = createRawContact("JohnDoe", "John", "Doe");
+        long rawContactId2 = createRawContact("JaneDuh", "Jane", "Duh");
+
+        assertTrue(rawContactId1 != rawContactId2);
+
+        long contactId1 = getContactIdByRawContactId(rawContactId1);
+        long contactId2 = getContactIdByRawContactId(rawContactId2);
+
+        assertTrue(contactId1 != contactId2);
+
+        String lookupKey1 = getContactLookupByContactId(contactId1);
+        String lookupKey2 = getContactLookupByContactId(contactId2);
+        assertFalse(lookupKey1.equals(lookupKey2));
+
+        Uri contactLookupUri1 = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey1);
+        Uri contactLookupUri2 = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey2);
+
+        ContactLoader.Result contact1 = assertLoadContact(contactLookupUri1);
+        ContactLoader.Result contact2 = assertLoadContact(contactLookupUri2);
+
+        assertEquals(contactId1, contact1.getId());
+        assertEquals(contactId2, contact2.getId());
+    }
+
+    public void testLoadContactWithContactLookupAndIdUri() {
+        // Use lookup-style Uris that also contain the Contact-ID
+        long rawContactId1 = createRawContact("JohnDoe", "John", "Doe");
+        long rawContactId2 = createRawContact("JaneDuh", "Jane", "Duh");
+
+        long contactId1 = getContactIdByRawContactId(rawContactId1);
+        long contactId2 = getContactIdByRawContactId(rawContactId2);
+
+        String lookupKey1 = getContactLookupByContactId(contactId1);
+        String lookupKey2 = getContactLookupByContactId(contactId2);
+
+        Uri contactLookupUri1 = ContentUris.withAppendedId(
+                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey1), contactId1);
+        Uri contactLookupUri2 = ContentUris.withAppendedId(
+                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey2), contactId2);
+
+        ContactLoader.Result contact1 = assertLoadContact(contactLookupUri1);
+        ContactLoader.Result contact2 = assertLoadContact(contactLookupUri2);
+
+        assertEquals(contactId1, contact1.getId());
+        assertEquals(contactId2, contact2.getId());
+    }
+
+    public void testLoadContactWithContactLookupWithIncorrectIdUri() {
+        // Use lookup-style Uris that contain incorrect Contact-ID
+        // (we want to ensure that still the correct contact is chosen)
+
+        long rawContactId1 = createRawContact("JohnDoe", "John", "Doe");
+        long rawContactId2 = createRawContact("JaneDuh", "Jane", "Duh");
+
+        long contactId1 = getContactIdByRawContactId(rawContactId1);
+        long contactId2 = getContactIdByRawContactId(rawContactId2);
+
+        String lookupKey1 = getContactLookupByContactId(contactId1);
+        String lookupKey2 = getContactLookupByContactId(contactId2);
+
+        long[] fakeIds = new long[] { 0, rawContactId1, rawContactId2, contactId1, contactId2 };
+
+        for (long fakeContactId : fakeIds) {
+            Uri contactLookupUri1 = ContentUris.withAppendedId(
+                    Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey1), fakeContactId);
+            Uri contactLookupUri2 = ContentUris.withAppendedId(
+                    Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey2), fakeContactId);
+
+            ContactLoader.Result contact1 = assertLoadContact(contactLookupUri1);
+            ContactLoader.Result contact2 = assertLoadContact(contactLookupUri2);
+
+            assertEquals(contactId1, contact1.getId());
+            assertEquals(contactId2, contact2.getId());
+        }
+    }
+}
diff --git a/tests/src/com/android/contacts/ContactListModeTest.java b/tests/src/com/android/contacts/ContactListModeTest.java
new file mode 100644
index 0000000..fe8ef5c
--- /dev/null
+++ b/tests/src/com/android/contacts/ContactListModeTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts;
+
+import 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;
+
+/**
+ * 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();
+        activity.onResume();        // Trigger the queries
+
+        assertEquals(3, activity.getListAdapter().getCount());
+    }
+}
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..bd2010e
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/mocks/ContactsMockContext.java
@@ -0,0 +1,58 @@
+/*
+ * 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;
+    }
+}
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..63b134a
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/mocks/MockContentProvider.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.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 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;
+            }
+
+            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 (projection != null) {
+                sb.append(Arrays.toString(selectionArgs));
+            } else {
+                sb.append("[]");
+            }
+        }
+        if (sortOrder != null) {
+            sb.append(" sort: '").append(sortOrder).append("'");
+        }
+        return sb.toString();
+    }
+}