am 2585921d: Merge "Fixing search in direct dial/message shortcut creation" into froyo
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 04d0cf4..a7eec1d 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">
 
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/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/strings.xml b/res/values/strings.xml
index 041440a..abdf496 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1134,4 +1134,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..22cb78f 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,7 +385,11 @@
 
     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;
 
@@ -423,8 +403,6 @@
 
     private Uri mGroupUri;
 
-    private long mQueryAggregateId;
-
     private ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>();
     private int  mWritableSourcesCnt;
     private int  mReadOnlySourcesCnt;
@@ -459,16 +437,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 +448,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 +576,19 @@
         }
     };
 
+    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);
+        }
+    };
+
     @Override
     protected void onCreate(Bundle icicle) {
         super.onCreate(icicle);
@@ -565,9 +597,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 +617,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 +638,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 +677,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 +713,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 +832,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 +857,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 +871,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 +912,7 @@
         getContentResolver().unregisterContentObserver(mProviderStatusObserver);
     }
 
-    private void setupListView() {
+    protected void setupListView(ContactItemListAdapter adapter) {
         final ListView list = getListView();
         final LayoutInflater inflater = getLayoutInflater();
 
@@ -868,7 +924,7 @@
         list.setDividerHeight(0);
         list.setOnCreateContextMenuListener(this);
 
-        mAdapter = new ContactItemListAdapter(this);
+        mAdapter = adapter;
         setListAdapter(mAdapter);
 
         if (list instanceof PinnedHeaderListView && mAdapter.getDisplaySectionHeadersEnabled()) {
@@ -897,29 +953,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 +975,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);
     }
@@ -1159,7 +1142,7 @@
         retryUpgrade.setOnClickListener(listener);
     }
 
-    private String getTextFilter() {
+    protected String getTextFilter() {
         if (mSearchEditText != null) {
             return mSearchEditText.getText().toString();
         }
@@ -1191,6 +1174,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 +1185,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 +1207,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 +1226,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 +1280,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 +1318,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 +1340,6 @@
      * search text edit.
      */
     protected void onSearchTextChanged() {
-        // Set the proper empty string
-        setEmptyText();
-
         Filter filter = mAdapter.getFilter();
         filter.filter(getTextFilter());
     }
@@ -1306,7 +1347,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 +1590,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 +1775,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 +1861,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 +1887,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 +1924,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 +2130,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 +2149,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 +2224,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 +2256,6 @@
 
     String[] getProjectionForQuery() {
         switch(mMode) {
-            case MODE_JOIN_CONTACT:
             case MODE_STREQUENT:
             case MODE_FREQUENT:
             case MODE_STARRED:
@@ -2228,6 +2278,7 @@
                 return LEGACY_PEOPLE_PROJECTION ;
             }
             case MODE_QUERY_PICK_PHONE:
+            case MODE_PICK_MULTIPLE_PHONES:
             case MODE_PICK_PHONE: {
                 return PHONES_PROJECTION;
             }
@@ -2314,7 +2365,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 +2393,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 +2457,10 @@
                     .build();
         }
 
+        startQuery(uri, projection);
+    }
+
+    protected void startQuery(Uri uri, String[] projection) {
         // Kick off the new query
         switch (mMode) {
             case MODE_GROUP:
@@ -2447,6 +2503,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 +2534,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 +2595,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 +2612,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 +2754,160 @@
         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
         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 +2916,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 +2943,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 +2953,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 +2998,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 +3048,6 @@
                 return IGNORE_ITEM_VIEW_TYPE;
             }
 
-            if (isShowAllContactsItemPosition(position)) {
-                return IGNORE_ITEM_VIEW_TYPE;
-            }
-
             if (isSearchAllContactsItemPosition(position)) {
                 return IGNORE_ITEM_VIEW_TYPE;
             }
@@ -2894,7 +3056,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 +3079,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 +3093,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 +3107,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 +3142,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 +3151,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 +3158,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 +3174,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 +3188,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 +3213,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 +3241,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 +3255,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 +3389,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 +3445,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 +3519,7 @@
         @Override
         public boolean areAllItemsEnabled() {
             return mMode != MODE_STARRED
-                && !mShowNumberOfContacts
-                && mSuggestionsCursorCount == 0;
+                && !mShowNumberOfContacts;
         }
 
         @Override
@@ -3362,10 +3530,6 @@
                 }
                 position--;
             }
-
-            if (mSuggestionsCursorCount > 0) {
-                return position != 0 && position != mSuggestionsCursorCount + 1;
-            }
             return position != mFrequentSeparatorPos;
         }
 
@@ -3382,7 +3546,7 @@
                 superCount++;
             }
 
-            if (mSearchMode) {
+            if (mSearchMode && mMode != MODE_PICK_MULTIPLE_PHONES) {
                 // Last element in the list is the "Find
                 superCount++;
             }
@@ -3393,12 +3557,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 +3583,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 +3603,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 +3616,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 +3713,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..85e75e6 100644
--- a/src/com/android/contacts/ImportVCardActivity.java
+++ b/src/com/android/contacts/ImportVCardActivity.java
@@ -39,6 +39,7 @@
 import android.pim.vcard.VCardEntryCounter;
 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;
@@ -155,7 +156,7 @@
     private class VCardReadThread extends Thread
             implements DialogInterface.OnCancelListener {
         private ContentResolver mResolver;
-        private VCardParser_V21 mVCardParser;
+        private VCardParser mVCardParser;
         private boolean mCanceled;
         private PowerManager.WakeLock mWakeLock;
         private Uri mUri;
@@ -220,13 +221,15 @@
                     boolean result;
                     try {
                         result = readOneVCardFile(targetUri,
-                                VCardConfig.DEFAULT_CHARSET, builderCollection, null, true, null);
+                                VCardConfig.DEFAULT_IMPORT_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);
+                                    VCardConfig.DEFAULT_IMPORT_CHARSET,
+                                    counter, detector, false, null);
                         } catch (VCardNestedException e2) {
                             result = false;
                             Log.e(LOG_TAG, "Must not reach here. " + e2);
@@ -265,7 +268,8 @@
 
                         VCardSourceDetector detector = new VCardSourceDetector();
                         try {
-                            if (!readOneVCardFile(targetUri, VCardConfig.DEFAULT_CHARSET,
+                            if (!readOneVCardFile(targetUri,
+                                    VCardConfig.DEFAULT_IMPORT_CHARSET,
                                     detector, null, true, mErrorFileNameList)) {
                                 continue;
                             }
@@ -337,7 +341,7 @@
             if (charset != null) {
                 builder = new VCardEntryConstructor(charset, charset, false, vcardType, mAccount);
             } else {
-                charset = VCardConfig.DEFAULT_CHARSET;
+                charset = VCardConfig.DEFAULT_IMPORT_CHARSET;
                 builder = new VCardEntryConstructor(null, null, false, vcardType, mAccount);
             }
             VCardEntryCommitter committer = new VCardEntryCommitter(mResolver);
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/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..5dabdc7
--- /dev/null
+++ b/src/com/android/contacts/activities/ContactDetailActivity.java
@@ -0,0 +1,175 @@
+/*
+ * 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.views.detail.ContactDetailView;
+import com.android.contacts.views.detail.ContactLoader;
+import com.android.contacts.views.detail.ContactLoader.Result;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+
+public class ContactDetailActivity extends Activity implements ContactLoader.Callbacks,
+        DialogManager.DialogShowingViewActivity {
+    private ContactDetailView mDetails;
+    private ContactLoader mLoader;
+    private Uri mUri;
+    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));
+
+        mUri = getIntent().getData();
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+
+        if (mLoader == null) {
+            // Look for a passed along loader and create a new one if it's not there
+            mLoader = (ContactLoader) getLastNonConfigurationInstance();
+            if (mLoader == null) {
+                mLoader = new ContactLoader(this, mUri);
+            }
+        }
+        mLoader.registerCallbacks(this);
+        mLoader.startLoading();
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+
+        // Let the loader know we're done with it
+        mLoader.unregisterCallbacks(this);
+
+        // The loader isn't getting passed along to the next instance so ask it to stop loading
+        // TODO: Readd this once we have framework support
+        /*if (!isChangingConfigurations()) {
+            mLoader.stopLoading();
+        }*/
+    }
+
+    @Override
+    public Object onRetainNonConfigurationInstance() {
+        // Pass the loader along to the next guy
+        Object result = mLoader;
+        mLoader = null;
+        return result;
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+
+        if (mLoader != null) {
+            mLoader.destroy();
+        }
+    }
+
+    public void onContactLoaded(Result contact) {
+        if (contact == ContactLoader.Result.NOT_FOUND) {
+            // Item has been deleted
+            Log.i(TAG, "No contact found. Closing activity");
+            finish();
+            return;
+        }
+        mDetails.setData(contact);
+    }
+
+    @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..89ae997
--- /dev/null
+++ b/src/com/android/contacts/mvcframework/CursorLoader.java
@@ -0,0 +1,117 @@
+/*
+ * 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<CursorLoader.Callbacks> {
+    private Context mContext;
+    private Cursor mCursor;
+    private ForceLoadContentObserver mObserver;
+    private boolean mClosed;
+
+    public interface Callbacks {
+        public void onCursorLoaded(Cursor cursor);
+    }
+
+    protected Context getContext() {
+        return mContext;
+    }
+
+    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;
+            if (mCallbacks != null) {
+                // A listener is register, notify them of the result
+                mCallbacks.onCursorLoaded(cursor);
+            }
+        }
+    }
+
+    public CursorLoader(Context context) {
+        mContext = context.getApplicationContext();
+        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) {
+            mCallbacks.onCursorLoaded(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..456b53a
--- /dev/null
+++ b/src/com/android/contacts/mvcframework/Loader.java
@@ -0,0 +1,93 @@
+/*
+ * 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.database.ContentObserver;
+import android.os.Handler;
+
+public abstract class Loader<E> {
+    protected E mCallbacks;
+
+    protected final class ForceLoadContentObserver extends ContentObserver {
+        public ForceLoadContentObserver() {
+            super(new Handler());
+        }
+
+        @Override
+        public boolean deliverSelfNotifications() {
+            return true;
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            forceLoad();
+        }
+    }
+
+    /**
+     * 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 registerCallbacks(E callbacks) {
+        if (mCallbacks != null) {
+            throw new IllegalStateException("There are already callbacks registered");
+        }
+        mCallbacks = callbacks;
+    }
+
+    /**
+     * Must be called from the UI thread
+     */
+    public void unregisterCallbacks(E callbacks) {
+        if (mCallbacks == null) {
+            throw new IllegalStateException("No callbacks register");
+        }
+        if (mCallbacks != callbacks) {
+            throw new IllegalArgumentException("Attempting to unregister the wrong callbacks");
+        }
+        mCallbacks = 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/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..fd68bd2
--- /dev/null
+++ b/src/com/android/contacts/views/detail/ContactDetailView.java
@@ -0,0 +1,1038 @@
+/*
+ * 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 views;
+
+            // Check to see if we can reuse convertView
+            if (convertView != null) {
+                v = convertView;
+                views = (ViewCache) v.getTag();
+            } else {
+                // Create a new view if needed
+                v = mInflater.inflate(R.layout.list_item_text_icons, parent, false);
+
+                // Cache the children
+                views = new ViewCache();
+                views.label = (TextView) v.findViewById(android.R.id.text1);
+                views.data = (TextView) v.findViewById(android.R.id.text2);
+                views.footer = (TextView) v.findViewById(R.id.footer);
+                views.actionIcon = (ImageView) v.findViewById(R.id.action_icon);
+                views.primaryIcon = (ImageView) v.findViewById(R.id.primary_icon);
+                views.presenceIcon = (ImageView) v.findViewById(R.id.presence_icon);
+                views.secondaryActionButton = (ImageView) v.findViewById(
+                        R.id.secondary_action_button);
+                views.secondaryActionButton.setOnClickListener(this);
+                views.secondaryActionDivider = v.findViewById(R.id.divider);
+                v.setTag(views);
+            }
+
+            // Update the entry in the view cache
+            views.entry = entry;
+
+            // Bind the data to the view
+            bindView(v, entry);
+            return v;
+        }
+
+        @Override
+        protected View newView(int position, ViewGroup parent) {
+            // getView() handles this
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        protected void bindView(View view, ViewEntry entry) {
+            final Resources resources = mContext.getResources();
+            ViewCache views = (ViewCache) view.getTag();
+
+            // Set the label
+            TextView label = views.label;
+            setMaxLines(label, entry.maxLabelLines);
+            label.setText(entry.label);
+
+            // Set the data
+            TextView data = views.data;
+            if (data != null) {
+                if (entry.mimetype.equals(Phone.CONTENT_ITEM_TYPE)
+                        || entry.mimetype.equals(Constants.MIME_SMS_ADDRESS)) {
+                    data.setText(PhoneNumberUtils.formatNumber(entry.data));
+                } else {
+                    data.setText(entry.data);
+                }
+                setMaxLines(data, entry.maxLines);
+            }
+
+            // Set the footer
+            if (!TextUtils.isEmpty(entry.footerLine)) {
+                views.footer.setText(entry.footerLine);
+                views.footer.setVisibility(View.VISIBLE);
+            } else {
+                views.footer.setVisibility(View.GONE);
+            }
+
+            // Set the primary icon
+            views.primaryIcon.setVisibility(entry.isPrimary ? View.VISIBLE : View.GONE);
+
+            // Set the action icon
+            ImageView action = views.actionIcon;
+            if (entry.actionIcon != -1) {
+                Drawable actionIcon;
+                if (entry.resPackageName != null) {
+                    // Load external resources through PackageManager
+                    actionIcon = mContext.getPackageManager().getDrawable(entry.resPackageName,
+                            entry.actionIcon, null);
+                } else {
+                    actionIcon = resources.getDrawable(entry.actionIcon);
+                }
+                action.setImageDrawable(actionIcon);
+                action.setVisibility(View.VISIBLE);
+            } else {
+                // Things should still line up as if there was an icon, so make it invisible
+                action.setVisibility(View.INVISIBLE);
+            }
+
+            // Set the presence icon
+            Drawable presenceIcon = ContactPresenceIconUtil.getPresenceIcon(
+                    mContext, entry.presence);
+            ImageView presenceIconView = views.presenceIcon;
+            if (presenceIcon != null) {
+                presenceIconView.setImageDrawable(presenceIcon);
+                presenceIconView.setVisibility(View.VISIBLE);
+            } else {
+                presenceIconView.setVisibility(View.GONE);
+            }
+
+            // Set the secondary action button
+            ImageView secondaryActionView = views.secondaryActionButton;
+            Drawable secondaryActionIcon = null;
+            if (entry.secondaryActionIcon != -1) {
+                secondaryActionIcon = resources.getDrawable(entry.secondaryActionIcon);
+            }
+            if (entry.secondaryIntent != null && secondaryActionIcon != null) {
+                secondaryActionView.setImageDrawable(secondaryActionIcon);
+                secondaryActionView.setTag(entry.secondaryIntent);
+                secondaryActionView.setVisibility(View.VISIBLE);
+                views.secondaryActionDivider.setVisibility(View.VISIBLE);
+            } else {
+                secondaryActionView.setVisibility(View.GONE);
+                views.secondaryActionDivider.setVisibility(View.GONE);
+            }
+        }
+
+        private void setMaxLines(TextView textView, int maxLines) {
+            if (maxLines == 1) {
+                textView.setSingleLine(true);
+                textView.setEllipsize(TextUtils.TruncateAt.END);
+            } else {
+                textView.setSingleLine(false);
+                textView.setMaxLines(maxLines);
+                textView.setEllipsize(null);
+            }
+        }
+
+        public void onClick(View v) {
+            Intent intent = (Intent) v.getTag();
+            mContext.startActivity(intent);
+        }
+    }
+
+    public boolean onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
+        inflater.inflate(R.menu.view, menu);
+        return true;
+    }
+
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        // Only allow edit when we have at least one raw_contact id
+        final boolean hasRawContact = (mRawContactIds.size() > 0);
+        menu.findItem(R.id.menu_edit).setEnabled(hasRawContact);
+
+        // Only allow share when unrestricted contacts available
+        menu.findItem(R.id.menu_share).setEnabled(!mAllRestricted);
+
+        return true;
+    }
+
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.menu_edit: {
+                if (mRawContactIds.size() > 0) {
+                    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) {
+        ViewEntry entry = ViewAdapter.getEntry(mSections, position, SHOW_SEPARATORS);
+        if (entry == null) {
+            signalError();
+            return;
+        }
+        Intent intent = entry.intent;
+        if (intent == null) {
+            signalError();
+            return;
+        }
+        try {
+            mContext.startActivity(intent);
+        } catch (ActivityNotFoundException e) {
+            Log.e(TAG, "No activity found for intent: " + intent);
+            signalError();
+        }
+    }
+
+    /**
+     * Signal an error to the user via a beep, or some other method.
+     */
+    private void signalError() {
+        Log.w(TAG, "Should warn the user but we can't because we do not have sonification APIs");
+    }
+
+    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..38e7a5d
--- /dev/null
+++ b/src/com/android/contacts/views/detail/ContactLoader.java
@@ -0,0 +1,353 @@
+/*
+ * 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.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.Callbacks> {
+    private Context mContext;
+    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;
+    }
+
+    final class LoadContactTask extends AsyncTask<Void, Void, Result> {
+        @Override
+        protected Result doInBackground(Void... args) {
+            final ContentResolver resolver = mContext.getContentResolver();
+            Result result = loadContactHeaderData(resolver, mLookupUri);
+            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, mLookupUri);
+                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;
+        }
+
+        /**
+         * 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 furether updates
+            if (mDestroyed) {
+                return;
+            }
+
+            mContact = result;
+            if (result != null) {
+                if (mObserver == null) {
+                    mObserver = new ForceLoadContentObserver();
+                }
+                Log.i(TAG, "Registering content observer for " + mLookupUri);
+                mContext.getContentResolver().registerContentObserver(mLookupUri, true, mObserver);
+                mCallbacks.onContactLoaded(result);
+            }
+        }
+    }
+
+    public ContactLoader(Context context, Uri lookupUri) {
+        mContext = context.getApplicationContext();
+        mLookupUri = lookupUri;
+    }
+
+    @Override
+    public void startLoading() {
+        if (mContact != null) {
+            mCallbacks.onContactLoaded(mContact);
+        } else {
+            forceLoad();
+        }
+    }
+
+    @Override
+    public void forceLoad() {
+        new LoadContactTask().execute((Void[])null);
+    }
+
+    @Override
+    public void stopLoading() {
+        mContact = null;
+        if (mObserver != null) {
+            mContext.getContentResolver().unregisterContentObserver(mObserver);
+        }
+    }
+
+    @Override
+    public void destroy() {
+        mContact = null;
+        mDestroyed = true;
+    }
+}