Merge change I394867d3 into eclair

* changes:
  Cancel the selection and move the cursor pass the wait/pause symbol.
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 68751db..b6a2045 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -195,11 +195,6 @@
             </intent-filter>
 
             <intent-filter>
-                <action android:name="com.android.contacts.action.JOIN_AGGREGATE" />
-                <category android:name="android.intent.category.DEFAULT" />
-            </intent-filter>
-
-            <intent-filter>
                 <action android:name="android.intent.action.INSERT_OR_EDIT" />
                 <category android:name="android.intent.category.DEFAULT" />
                 <data android:mimeType="vnd.android.cursor.item/person" />
@@ -239,6 +234,18 @@
             />
         </activity>
 
+        <!-- An activity for joining contacts -->
+        <activity android:name="ContactsListActivity$JoinContactActivity"
+            android:theme="@style/TallTitleBarTheme"
+            android:clearTaskOnLaunch="true"
+        >
+            <intent-filter>
+                <action android:name="com.android.contacts.action.JOIN_AGGREGATE" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
+
         <!-- Used to select display and sync groups -->
         <activity android:name=".ui.DisplayGroupsActivity" android:label="@string/displayGroups" />
 
@@ -419,5 +426,7 @@
         <activity android:name=".ImportVCardActivity"
             android:theme="@style/BackgroundOnly" />
 
+        <activity android:name=".ExportVCardActivity"
+            android:theme="@style/BackgroundOnly" />
     </application>
 </manifest>
diff --git a/res/drawable-hdpi/ic_launcher_contacts.png b/res/drawable-hdpi/ic_launcher_contacts.png
new file mode 100644
index 0000000..73f5451
--- /dev/null
+++ b/res/drawable-hdpi/ic_launcher_contacts.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_launcher_folder_live_contacts_phone.png b/res/drawable-hdpi/ic_launcher_folder_live_contacts_phone.png
new file mode 100644
index 0000000..70ce098
--- /dev/null
+++ b/res/drawable-hdpi/ic_launcher_folder_live_contacts_phone.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_launcher_folder_live_contacts_starred.png b/res/drawable-hdpi/ic_launcher_folder_live_contacts_starred.png
new file mode 100644
index 0000000..a0c01ce
--- /dev/null
+++ b/res/drawable-hdpi/ic_launcher_folder_live_contacts_starred.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_launcher_phone.png b/res/drawable-hdpi/ic_launcher_phone.png
new file mode 100644
index 0000000..47186c3
--- /dev/null
+++ b/res/drawable-hdpi/ic_launcher_phone.png
Binary files differ
diff --git a/res/drawable/section_dark.9.png b/res/drawable/section_dark.9.png
new file mode 100644
index 0000000..5d1e1ae
--- /dev/null
+++ b/res/drawable/section_dark.9.png
Binary files differ
diff --git a/res/layout-finger/contact_card_layout.xml b/res/layout-finger/contact_card_layout.xml
index 3f090f5..fe931ec 100644
--- a/res/layout-finger/contact_card_layout.xml
+++ b/res/layout-finger/contact_card_layout.xml
@@ -36,6 +36,16 @@
             android:layout_width="fill_parent"
             android:layout_height="wrap_content"/>
         
+        <TextView android:id="@+id/account_name"
+            style="?android:attr/listSeparatorTextViewStyle"
+            android:textColor="@*android:color/dim_foreground_dark"
+            android:textSize="12sp"
+            android:textStyle="normal"
+            android:background="@drawable/section_dark"
+            android:paddingLeft="7dp"
+            android:gravity="left|center_vertical"
+            android:visibility="gone" />
+
         <FrameLayout android:id="@android:id/tabcontent"
             android:layout_width="fill_parent"
             android:layout_height="0dip"
diff --git a/res/layout-finger/contacts_list_content_join.xml b/res/layout-finger/contacts_list_content_join.xml
new file mode 100644
index 0000000..ce82d2c
--- /dev/null
+++ b/res/layout-finger/contacts_list_content_join.xml
@@ -0,0 +1,65 @@
+<?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:layout_width="fill_parent"
+        android:layout_height="fill_parent"
+        android:orientation="vertical">
+
+    <LinearLayout
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content" 
+            android:orientation="horizontal"
+            android:background="@*android:drawable/title_bar_medium"
+            android:padding="5dip"
+            android:gravity="center_vertical"
+            >
+    
+        <ImageView
+            android:layout_width="48dip"
+            android:layout_height="48dip"
+            android:src="@drawable/ic_menu_merge"
+            android:gravity="center"
+            android:scaleType="fitCenter"
+        />
+        <LinearLayout
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            android:paddingLeft="10dip">
+            <TextView
+                android:layout_width="fill_parent"
+                android:layout_height="wrap_content"
+                android:text="@string/titleJoinContactDataWith"
+                android:textAppearance="?android:attr/textAppearanceMedium"
+            />
+            <TextView
+                android:layout_width="fill_parent"
+                android:layout_height="wrap_content"
+                android:text="@string/blurbJoinContactDataWith"
+                android:textAppearance="?android:attr/textAppearanceSmall"
+            />
+        </LinearLayout>
+    </LinearLayout>
+
+    <ListView android:id="@android:id/list"
+            android:layout_width="fill_parent"
+            android:layout_height="fill_parent"
+            android:fastScrollEnabled="true"
+    />
+</LinearLayout>
+
diff --git a/res/layout-finger/total_contacts.xml b/res/layout-finger/total_contacts.xml
new file mode 100644
index 0000000..1221ef3
--- /dev/null
+++ b/res/layout-finger/total_contacts.xml
@@ -0,0 +1,29 @@
+<?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.
+-->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/totalContactsText"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    style="?android:attr/listSeparatorTextViewStyle"
+    android:textColor="@*android:color/dim_foreground_dark"
+    android:textSize="12sp"
+    android:textStyle="normal"
+    android:background="@drawable/section_dark"
+    android:paddingLeft="7dp"
+    android:gravity="left|center_vertical"
+    android:visibility="gone"
+/>
\ No newline at end of file
diff --git a/res/layout/act_edit.xml b/res/layout/act_edit.xml
index 250a0e1..47f1454 100644
--- a/res/layout/act_edit.xml
+++ b/res/layout/act_edit.xml
@@ -37,6 +37,16 @@
             android:layout_width="fill_parent"
             android:layout_height="wrap_content" />
 
+        <TextView android:id="@+id/account_name"
+            style="?android:attr/listSeparatorTextViewStyle"
+            android:textColor="@*android:color/dim_foreground_dark"
+            android:textSize="12sp"
+            android:textStyle="normal"
+            android:background="@drawable/section_dark"
+            android:paddingLeft="7dp"
+            android:gravity="left|center_vertical"
+            android:visibility="gone" />
+        
         <include
             android:id="@android:id/tabcontent"
             android:layout_width="fill_parent"
diff --git a/res/values/ids.xml b/res/values/ids.xml
index 1af6162..cf1d419 100644
--- a/res/values/ids.xml
+++ b/res/values/ids.xml
@@ -42,8 +42,7 @@
     <item type="id" name="dialog_error_with_message" />
 
     <!-- For ExportVCard -->
-    <item type="id" name="dialog_confirm_export_vcard" />
-    <item type="id" name="dialog_exporting_vcard" />
+    <item type="id" name="dialog_export_confirmation" />
     <item type="id" name="dialog_fail_to_export_with_reason" />
 
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 2cbfb02..3fdcf76 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -130,7 +130,7 @@
 
     <!-- Menu item that joins an aggregate with another aggregate -->
     <string name="menu_joinAggregate">Join</string>
-    
+
     <!-- Menu item to toggle the tabs that show where the contact data comes from. -->
     <string name="menu_showSources">Show sources</string>
     <string name="menu_hideSources">Hide sources</string>
@@ -138,6 +138,13 @@
     <!-- Activity title for the Join Contact list -->
     <string name="titleJoinAggregate">Join contact</string>
 
+    <!-- Heading of the Join Contact screen -->
+    <string name="titleJoinContactDataWith">Join contacts</string>
+
+    <!-- Info blurb on the Join Contact screen -->
+    <string name="blurbJoinContactDataWith">Contact data will be combined with
+        another contact selected from the list.</string>
+
     <!-- List separator for the Join Contact list: Suggestions -->
     <string name="separatorJoinAggregateSuggestions">Suggestions</string>
 
@@ -301,6 +308,9 @@
     <!-- The text displayed when the contacts list is empty while displaying all contacts -->
     <string name="noContacts">No contacts.</string>
 
+    <!-- The text displayed when the contacts list is empty while displaying results after searching contacts -->
+    <string name="noMatchingContacts">No matching contacts found.</string>
+
     <!-- The text displayed when the contacts list is empty while displaying only contacts that have phone numbers -->
     <string name="noContactsWithPhoneNumbers">No contacts with phone numbers.</string>
 
@@ -324,7 +334,7 @@
     <string name="showFilterPhonesDescrip">Only display contacts that have phone numbers</string>
 
     <!-- The header over the list of all contacts groups -->
-    <string name="headerContactGroups">Contact groups</string>
+    <string name="headerContactGroups">Choose contacts to display</string>
 
     <!-- The description of a group with the total number of contacts -->
     <plurals name="groupDescrip">
@@ -351,8 +361,8 @@
     <!-- Displayed in a spinner dialog after the user creates a contact and it's being saved to the database -->
     <string name="savingContact">Saving contact\u2026</string>
 
-    <!-- Displayed in a spinner dialog as user changes to display groups are saved -->
-    <string name="savingDisplayGroups">Saving display groups\u2026</string>
+    <!-- Displayed in a spinner dialog as user changes to display options are saved -->
+    <string name="savingDisplayGroups">Saving display options\u2026</string>
 
     <!-- Toast displayed when a contact is created -->
     <string name="contactCreatedToast">Contact created.</string>
@@ -405,6 +415,12 @@
     <!-- Section header in the Edit Contacts screen for the "Add more items" button -->
     <string name="listSeparatorMore_edit">More</string>
 
+    <!-- Displayed at the top of the contacts showing the total number of contacts visible when "Only contacts with phones" is selected -->
+    <string name="listTotalPhoneContacts"><xliff:g id="num">%s</xliff:g> contacts with phone numbers</string>
+
+    <!-- Displayed at the top of the contacts showing the total number of contacts visible when "Only contacts with phones" not selected -->
+    <string name="listTotalAllContacts"><xliff:g id="num">%s</xliff:g> contacts</string>
+
     <!-- The description text for the social activity stream tab. Space is limited for this string, so the shorter the better -->
     <string name="socialStreamIconLabel">Social</string>
 
@@ -810,6 +826,12 @@
     <!-- Message while reading multiple vCard files "(current number) of (total number) files" The order of "current number" and "total number" cannot be changed (like "total: (total number), current: (current number)")-->
     <string name="reading_vcard_files"><xliff:g id="current_number">%s</xliff:g> of <xliff:g id="total_number">%s</xliff:g> files</string>
 
+    <!-- The message for exporting all contacts in the phone, without caring its Account -->
+    <string name="export_all_contacts">All contacts</string>
+
+    <!-- The message for exporting only contacts is the phone locally, which are not associated with any account -->
+    <string name="export_phone_local_only">Locally stored contacts</string>
+
     <!-- The menu item that launches VCard export activity -->
     <string name="export_contact_list">Export contacts</string>
 
@@ -825,6 +847,9 @@
     <!-- Dialog message shown when exporting Contact data failed -->
     <string name="exporting_contact_failed_message">Failed to export contact data.\nReason for failure: \"<xliff:g id="fail_reason">%s</xliff:g>\"</string>
 
+    <!-- The failed reason: "There is no exportable contact" -->
+    <string name="fail_reason_no_exportable_contact">There is no exportable contact</string>
+
     <!-- The failed reason: "Too many vcard files on the SD Card" -->
     <string name="fail_reason_too_many_vcard">Too many vCard files on the SD card</string>
 
@@ -846,6 +871,15 @@
     <!-- The failed reason: "Error occured during export" -->
     <string name="fail_reason_error_occurred_during_export">Error occured during export: \"<xliff:g id="exact_reason">%s</xliff:g>\"</string>
 
+    <!-- The error reason the vCard composer emits: "Failed to get database information" -->
+    <string name="composer_failed_to_get_database_infomation">Failed to get database information</string>
+
+    <!-- The error reason the vCard composer emits: "There is no exportable contact. You might choose un-exportable data" -->
+    <string name="composer_has_no_exportable_contact">There is no exportable contact. You might choose un-exportable data</string>
+
+    <!-- The error reason the vCard composer emits: "The vCard composer object is not correctly initialized" -->
+    <string name="composer_not_initialized">The vCard composer is not correctly initialized</string>
+
     <!-- The failed reason: "Could not open a specific file" -->
     <string name="fail_reason_could_not_open_file">Could not open \"<xliff:g id="file_name">%s</xliff:g>\": <xliff:g id="exact_reason">%s</xliff:g></string>
 
@@ -1112,10 +1146,13 @@
     <string name="name_phonetic_middle">Phonetic middle name</string>
     <!-- Field title for the phonetic family name of a contact -->
     <string name="name_phonetic_family">Phonetic family name</string>
-    
+
     <!-- The title for the action to remove a contact from an aggregated contact -->
     <string name="split_label">Split</string>
     <!-- The explanation of what "split" will do. This needs word-smithing. -->
     <string name="split_explanation">Make this data its own contact.</string>
+    
+    <!-- Formatting string for account name -->
+    <string name="account_name_format">From account: <xliff:g id="account" example="user@gmail.com">%s</xliff:g></string>
 
 </resources>
diff --git a/src/com/android/contacts/Collapser.java b/src/com/android/contacts/Collapser.java
index db1da1f..3872dfd 100644
--- a/src/com/android/contacts/Collapser.java
+++ b/src/com/android/contacts/Collapser.java
@@ -39,38 +39,42 @@
      */
     public interface Collapsible<T> {
         public boolean collapseWith(T t);
-        public String getCollapseKey();
+        public boolean shouldCollapseWith(T t);
     }
 
     /**
      * Collapses a list of Collapsible items into a list of collapsed items. Items are collapsed
-     * if they produce equal collapseKeys {@Link Collapsible#getCollapseKey()}, and are collapsed
-     * through the {@Link Collapsible#doCollapseWith(Object)} function implemented by the data item.
+     * if {@link Collapsible#shouldCollapseWith(Object) return strue, and are collapsed
+     * through the {@Link Collapsible#collapseWith(Object)} function implemented by the data item.
      *
      * @param list ArrayList of Objects of type <T extends Collapsible<T>> to be collapsed.
      */
     public static <T extends Collapsible<T>> void collapseList(ArrayList<T> list) {
-        HashMap<String, T> collapseMap = new HashMap<String, T>();
-        ArrayList<String> collapseKeys = new ArrayList<String>();
 
         int listSize = list.size();
-        for (int j = 0; j < listSize; j++) {
-            T entry = list.get(j);
-            String collapseKey = entry.getCollapseKey();
-            if (!collapseMap.containsKey(collapseKey)) {
-                collapseMap.put(collapseKey, entry);
-                collapseKeys.add(collapseKey);
-            } else {
-                collapseMap.get(collapseKey).collapseWith(entry);
+
+        for (int i = 0; i < listSize; i++) {
+            T iItem = list.get(i);
+            if (iItem != null) {
+                for (int j = i + 1; j < listSize; j++) {
+                    T jItem = list.get(j);
+                    if (jItem != null) {
+                        if (iItem.shouldCollapseWith(jItem)) {
+                            iItem.collapseWith(jItem);
+                            list.set(j, null);
+                        }
+                    }
+                }
             }
         }
 
-        if (collapseKeys.size() < listSize) {
-            list.clear();
-            Iterator<String> itr = collapseKeys.iterator();
-            while (itr.hasNext()) {
-                list.add(collapseMap.get(itr.next()));
+        // Remove the null items
+        Iterator<T> itr = list.iterator();
+        while (itr.hasNext()) {
+            if (itr.next() == null) {
+                itr.remove();
             }
         }
+
     }
 }
diff --git a/src/com/android/contacts/ContactsListActivity.java b/src/com/android/contacts/ContactsListActivity.java
index 072725a..6d2ea57 100644
--- a/src/com/android/contacts/ContactsListActivity.java
+++ b/src/com/android/contacts/ContactsListActivity.java
@@ -16,11 +16,10 @@
 
 package com.android.contacts;
 
-import com.android.contacts.model.ContactsSource;
-import com.android.contacts.model.GoogleSource;
 import com.android.contacts.model.Sources;
 import com.android.contacts.ui.DisplayGroupsActivity;
 import com.android.contacts.ui.DisplayGroupsActivity.Prefs;
+import com.android.contacts.util.AccountSelectionUtil;
 import com.android.contacts.util.Constants;
 
 import android.accounts.Account;
@@ -117,8 +116,13 @@
 /**
  * Displays a list of contacts. Usually is embedded into the ContactsActivity.
  */
-public final class ContactsListActivity extends ListActivity implements
+public class ContactsListActivity extends ListActivity implements
         View.OnCreateContextMenuListener, View.OnClickListener {
+
+    public static class JoinContactActivity extends ContactsListActivity {
+
+    }
+
     private static final String TAG = "ContactsListActivity";
 
     private static final boolean ENABLE_ACTION_ICON_OVERLAYS = true;
@@ -175,11 +179,13 @@
     static final int MODE_MASK_SHOW_CALL_BUTTON = 0x02000000;
     /** Mask to disable fasttrack (images will show as normal images) */
     static final int MODE_MASK_DISABLE_FASTTRACK = 0x01000000;
+    /** Mask to show the total number of contacts at the top */
+    static final int MODE_MASK_SHOW_NUMBER_OF_CONTACTS = 0x00800000;
 
     /** Unknown mode */
     static final int MODE_UNKNOWN = 0;
     /** Default mode */
-    static final int MODE_DEFAULT = 4 | MODE_MASK_SHOW_PHOTOS;
+    static final int MODE_DEFAULT = 4 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_SHOW_NUMBER_OF_CONTACTS;
     /** Custom mode */
     static final int MODE_CUSTOM = 8;
     /** Show all starred contacts */
@@ -301,15 +307,6 @@
 
     static final String KEY_PICKER_MODE = "picker_mode";
 
-    /*
-     * TODO: Will be commented in the really near future. Similar commented out codes will be done.
-     *       These are for make vCard exporter code understard two additional options:
-     *       "export contacts in the phone only" and
-     *       "export all contacts without caring Account information".
-     * static final Account sExportPhoneLocalAccount;
-     * static final Account sExportAllAccount;
-     */
-
     private ContactItemListAdapter mAdapter;
 
     int mMode = MODE_DEFAULT;
@@ -351,8 +348,6 @@
      */
     private String mQueryData;
 
-    private VCardExporter mVCardExporter;
-
     private static final String CLAUSE_ONLY_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1";
     private static final String CLAUSE_ONLY_PHONES = Contacts.HAS_PHONE_NUMBER + "=1";
 
@@ -362,8 +357,6 @@
     static {
         sContactsIdMatcher = new UriMatcher(UriMatcher.NO_MATCH);
         sContactsIdMatcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID);
-        /*sExportPhoneLocalAccount = new Account("Phone-local Account", "phone-local");
-        sExportAllAccount = new Account("All account", "all");*/
     }
 
     private class DeleteClickListener implements DialogInterface.OnClickListener {
@@ -394,8 +387,6 @@
         final String action = intent.getAction();
         mMode = MODE_UNKNOWN;
 
-        setContentView(R.layout.contacts_list_content);
-
         Log.i(TAG, "Called with action: " + action);
         if (UI.LIST_DEFAULT.equals(action)) {
             mMode = MODE_DEFAULT;
@@ -541,6 +532,7 @@
             return;
         }
 
+
         if (JOIN_AGGREGATE.equals(action)) {
             mMode = MODE_JOIN_CONTACT;
             mQueryAggregateId = intent.getLongExtra(EXTRA_AGGREGATE_ID, -1);
@@ -550,16 +542,21 @@
                 setResult(RESULT_CANCELED);
                 finish();
             }
-
-            setTitle(R.string.titleJoinAggregate);
         }
 
         if (mMode == MODE_UNKNOWN) {
             mMode = MODE_DEFAULT;
         }
 
+        if (mMode == MODE_JOIN_CONTACT) {
+            setContentView(R.layout.contacts_list_content_join);
+        } else {
+            setContentView(R.layout.contacts_list_content);
+        }
+
         // Setup the UI
         final ListView list = getListView();
+
         // Tell list view to not show dividers. We'll do it ourself so that we can *not* show
         // them when an A-Z headers is visible.
         list.setDividerHeight(0);
@@ -569,9 +566,14 @@
             list.setTextFilterEnabled(true);
         }
 
+        final LayoutInflater inflater = getLayoutInflater();
+        if ((mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0) {
+            View totalContacts = inflater.inflate(R.layout.total_contacts, list, false);
+            list.addHeaderView(totalContacts);
+        }
+
         if ((mMode & MODE_MASK_CREATE_NEW) != 0) {
             // Add the header for creating a new contact
-            final LayoutInflater inflater = getLayoutInflater();
             View header = inflater.inflate(R.layout.create_new_contact, list, false);
             list.addHeaderView(header);
         }
@@ -583,6 +585,11 @@
         setListAdapter(mAdapter);
         getListView().setOnScrollListener(mAdapter);
 
+        if ((mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0) {
+            TextView totalContacts = (TextView) findViewById(R.id.totalContactsText);
+            totalContacts.setVisibility(View.VISIBLE);
+        }
+
         // We manually save/restore the listview state
         list.setSaveEnabled(false);
 
@@ -625,6 +632,10 @@
     }
 
     private void setEmptyText() {
+        if (mMode == MODE_JOIN_CONTACT) {
+            return;
+        }
+
         TextView empty = (TextView) findViewById(R.id.emptyText);
         int gravity = Gravity.NO_GRAVITY;
 
@@ -633,6 +644,8 @@
             gravity = Gravity.CENTER;
         } else if (mMode == MODE_STREQUENT || mMode == MODE_STARRED) {
             empty.setText(getText(R.string.noFavoritesHelpText));
+        } else if (mMode == MODE_QUERY) {
+             empty.setText(getText(R.string.noMatchingContacts));
         } else {
             boolean hasSim = ((TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE))
                     .hasIccCard();
@@ -747,6 +760,7 @@
         // be there and show up while the new query is happening. After the async query finished
         // in response to onRestart() setLoading(false) will be called.
         mAdapter.setLoading(true);
+        mAdapter.setSuggestionsCursor(null);
         mAdapter.changeCursor(null);
         mAdapter.clearImageFetching();
 
@@ -755,21 +769,6 @@
             SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
             searchManager.stopSearch();
         }
-
-        // When the orientation is changed or Home button is pressed, onStop() is called.
-        // Then, we stop exporting just for safety.
-        //
-        // Technically, it is because the dialog displaying the current status of export is
-        // closed on this method call and we cannot reliably restore the dialog in the current
-        // implementation, while the thread for exporting vCard is working at that time, without
-        // showing its status to users :(
-        // Also, it is probably not a strong requirment for us to both
-        // - enable users to press Home, slide hardware keyboard during the export
-        // - and also let vCard export to keep working.
-        if (mVCardExporter != null) {
-            mVCardExporter.cancelExport();
-            mVCardExporter = null;
-        }
     }
 
     @Override
@@ -812,7 +811,7 @@
                 return true;
             }
             case R.id.menu_import_export: {
-                showDialog(R.id.dialog_import_export);
+                displayImportExportDialog();
                 return true;
             }
             case R.id.menu_accounts: {
@@ -830,30 +829,9 @@
     @Override
     protected Dialog onCreateDialog(int id) {
         switch (id) {
-            case R.id.dialog_import_export: {
-                return createImportExportDialog();
-            }
             case R.string.import_from_sim:
-            case R.string.import_from_sdcard:
-            case R.string.export_to_sdcard: {
-                return createSelectAccountDialog(id);
-            }
-            case R.string.fail_reason_too_many_vcard: {
-                return new AlertDialog.Builder(this)
-                    .setTitle(R.string.exporting_contact_failed_title)
-                    .setMessage(getString(R.string.exporting_contact_failed_message,
-                            getString(R.string.fail_reason_too_many_vcard)))
-                    .setPositiveButton(android.R.string.ok, null)
-                .create();
-            }
-            case R.id.dialog_confirm_export_vcard: {
-                return mVCardExporter.getExportConfirmationDialog();
-            }
-            case R.id.dialog_exporting_vcard: {
-                return mVCardExporter.getExportingVCardDialog();
-            }
-            case R.id.dialog_fail_to_export_with_reason: {
-                return mVCardExporter.getErrorDialogWithReason();
+            case R.string.import_from_sdcard: {
+                return AccountSelectionUtil.getSelectAccountDialog(this, id);
             }
             case R.id.dialog_sdcard_not_found: {
                 AlertDialog.Builder builder = new AlertDialog.Builder(this)
@@ -866,104 +844,11 @@
         return super.onCreateDialog(id);
     }
 
-    private class AccountSelectedListener
-        implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
-
-        final private List<Account> mAccountList;
-        final private int mResId;
-
-        public AccountSelectedListener(List<Account> accountList, int resId) {
-            if (accountList == null || accountList.size() == 0) {
-                Log.e(TAG, "The size of Account list is 0.");
-            }
-            mAccountList = accountList;
-            mResId = resId;
-        }
-
-        public void onClick(DialogInterface dialog, int which) {
-            dialog.dismiss();
-            switch (mResId) {
-                case R.string.import_from_sim: {
-                    doImportFromSim(mAccountList.get(which));
-                    break;
-                }
-                case R.string.import_from_sdcard: {
-                    doImportFromSdCard(mAccountList.get(which));
-                    break;
-                }
-                /*case R.string.export_to_sdcard: {
-                }*/
-            }
-        }
-
-        public void onCancel(DialogInterface dialog) {
-            dialog.dismiss();
-        }
-    }
-
-    private Dialog createSelectAccountDialog(int resId) {
-        final boolean isExport = (resId == R.string.export_to_sdcard);
-        final boolean displayWritableOnly = !isExport;
-
-        final Sources sources = Sources.getInstance(this);
-        final List<Account> accountList = sources.getAccounts(displayWritableOnly);
-
-        /*if (isExport) {
-            accountList.add(sExportPhoneLocalAccount);
-            accountList.add(sExportAllAccount);
-        }*/
-
-        // Assume accountList.size() > 1
-
-        // Wrap our context to inflate list items using correct theme
-        final Context dialogContext = new ContextThemeWrapper(
-                this, android.R.style.Theme_Light);
-        final LayoutInflater dialogInflater = (LayoutInflater)dialogContext
-                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-        final ArrayAdapter<Account> accountAdapter =
-            new ArrayAdapter<Account>(this, android.R.layout.simple_list_item_2, accountList) {
-
-            @Override
-            public View getView(int position, View convertView,
-                    ViewGroup parent) {
-                if (convertView == null) {
-                    convertView = dialogInflater.inflate(
-                            android.R.layout.simple_list_item_2,
-                            parent, false);
-                }
-
-                // TODO: show icon along with title
-                final TextView text1 =
-                        (TextView)convertView.findViewById(android.R.id.text1);
-                final TextView text2 =
-                        (TextView)convertView.findViewById(android.R.id.text2);
-
-                final Account account = this.getItem(position);
-                final ContactsSource source =
-                    sources.getInflatedSource(account.type,
-                            ContactsSource.LEVEL_SUMMARY);
-
-                text1.setText(account.name);
-                text2.setText(source.getDisplayLabel(ContactsListActivity.this));
-
-                return convertView;
-            }
-        };
-
-        AccountSelectedListener listener = new AccountSelectedListener(accountList, resId);
-        final AlertDialog.Builder builder =
-                new AlertDialog.Builder(this)
-                    .setTitle(R.string.dialog_new_contact_account)
-                    .setSingleChoiceItems(accountAdapter, 0, listener)
-                    .setOnCancelListener(listener);
-        return builder.create();
-    }
-
     /**
      * Create a {@link Dialog} that allows the user to pick from a bulk import
      * or bulk export task across all contacts.
      */
-    private Dialog createImportExportDialog() {
+    private void displayImportExportDialog() {
         // Wrap our context to inflate list items using correct theme
         final Context dialogContext = new ContextThemeWrapper(this, android.R.style.Theme_Light);
         final Resources res = dialogContext.getResources();
@@ -996,7 +881,8 @@
             adapter.add(R.string.export_to_sdcard);
         }
 
-        final DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() {
+        final DialogInterface.OnClickListener clickListener =
+                new DialogInterface.OnClickListener() {
             public void onClick(DialogInterface dialog, int which) {
                 dialog.dismiss();
 
@@ -1004,12 +890,13 @@
                 switch (resId) {
                     case R.string.import_from_sim:
                     case R.string.import_from_sdcard: {
-                        handleImportExportRequest(resId);
+                        handleImportRequest(resId);
                         break;
                     }
                     case R.string.export_to_sdcard: {
-                        // TODO: use handleImportExportRequest()
-                        doExportToSdCard();
+                        Context context = ContactsListActivity.this;
+                        Intent exportIntent = new Intent(context, ExportVCardActivity.class);
+                        context.startActivity(exportIntent);
                         break;
                     }
                     default: {
@@ -1020,93 +907,27 @@
             }
         };
 
-        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
-        builder.setTitle(R.string.dialog_import_export);
-        builder.setNegativeButton(android.R.string.cancel, null);
-        builder.setSingleChoiceItems(adapter, -1, clickListener);
-        return builder.create();
+        new AlertDialog.Builder(this)
+            .setTitle(R.string.dialog_import_export)
+            .setNegativeButton(android.R.string.cancel, null)
+            .setSingleChoiceItems(adapter, -1, clickListener)
+            .show();
     }
 
-    private void handleImportExportRequest(int resId) {
-        /*final boolean isExport = (resId == R.string.export_to_sdcard);
-        if (isExport) {
-            // We're sure there are at least two Account ("phone-local" and "all").
-            // Go to account selection every time.
-            showDialog(R.string.export_to_sdcard);
-            return;
-        }*/
-
-        // As for import, there's three possibilities
+    private void handleImportRequest(int resId) {
+        // There's three possibilities:
         // - more than one accounts -> ask the user
         // - just one account -> use the account without asking the user
         // - no account -> use phone-local storage without asking the user
-
         final Sources sources = Sources.getInstance(this);
         final List<Account> accountList = sources.getAccounts(true);
         final int size = accountList.size();
-        Account account;
         if (size > 1) {
             showDialog(resId);
             return;
-        } else if (size == 1) {
-            account = accountList.get(0);
-        } else {
-            account = null;
-        }
-        switch (resId) {
-            case R.string.import_from_sim: {
-                doImportFromSim(account);
-                break;
-            }
-            case R.string.import_from_sdcard: {
-                doImportFromSdCard(account);
-                break;
-            }
-            /*case R.string.export_to_sdcard: {
-                doExportToSdCard(account);
-                break;
-            }*/
-        }
-    }
-
-    private void doImportFromSim(Account account) {
-        if (account != null) {
-            GoogleSource.createMyContactsIfNotExist(account, this);
         }
 
-        Intent importIntent = new Intent(Intent.ACTION_VIEW);
-        importIntent.setType("vnd.android.cursor.item/sim-contact");
-        if (account != null) {
-            importIntent.putExtra("account_name", account.name);
-            importIntent.putExtra("account_type", account.type);
-        }
-        importIntent.setClassName("com.android.phone", "com.android.phone.SimContacts");
-        startActivity(importIntent);
-    }
-
-    private void doImportFromSdCard(Account account) {
-        if (account != null) {
-            GoogleSource.createMyContactsIfNotExist(account, this);
-        }
-
-        Intent importIntent = new Intent(this, ImportVCardActivity.class);
-        if (account != null) {
-            importIntent.putExtra("account_name", account.name);
-            importIntent.putExtra("account_type", account.type);
-        }
-        startActivity(importIntent);
-    }
-
-    private void doExportToSdCard() {
-        mVCardExporter = new VCardExporter(this);
-        mVCardExporter.startExportVCardToSdCard();
-    }
-
-    /**
-     * Used when VCardExporter finishes its exporting.
-     */
-    /* package */ void removeReferenceToVCardExporter() {
-        mVCardExporter = null;
+        AccountSelectionUtil.doImport(this, resId, (size == 1 ? accountList.get(0) : null));
     }
 
     @Override
@@ -1273,6 +1094,13 @@
                 getSystemService(Context.INPUT_METHOD_SERVICE);
         inputMethodManager.hideSoftInputFromWindow(mList.getWindowToken(), 0);
 
+        if ((mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0) {
+            if (position == 0) {
+                return;
+            }
+            position--;
+        }
+
         if (mMode == MODE_INSERT_OR_EDIT_CONTACT) {
             Intent intent;
             if (position == 0) {
@@ -1795,7 +1623,7 @@
             case MODE_JOIN_CONTACT:
                 mQueryHandler.setLoadingJoinSuggestions(true);
                 mQueryHandler.startQuery(QUERY_TOKEN, null, getJoinSuggestionsUri(null), projection,
-                        Contacts._ID + " != " + mQueryAggregateId, null, null);
+                        null, null, null);
                 break;
         }
     }
@@ -1871,8 +1699,8 @@
                         null, null);
                 mAdapter.setSuggestionsCursor(cursor);
                 return resolver.query(getContactFilterUri(filter), projection,
-                        Contacts._ID + " != " + mQueryAggregateId, null,
-                        getSortOrder(projection));
+                        Contacts._ID + " != " + mQueryAggregateId + " AND " + CLAUSE_ONLY_VISIBLE,
+                        null, getSortOrder(projection));
             }
         }
         throw new UnsupportedOperationException("filtering not allowed in mode " + mMode);
@@ -1929,7 +1757,7 @@
                             getColumnIndex(Phone.IS_SUPER_PRIMARY)) != 0) {
                         // Found super primary, call it.
                         phone = phonesCursor.
-                        getString(phonesCursor.getColumnIndex(Phone.NUMBER));
+                                getString(phonesCursor.getColumnIndex(Phone.NUMBER));
                         break;
                     }
                 }
@@ -2012,7 +1840,8 @@
 
                     startQuery(QUERY_TOKEN, null, activity.getContactFilterUri(activity.mQuery),
                             CONTACTS_SUMMARY_PROJECTION,
-                            Contacts._ID + " != " + activity.mQueryAggregateId, null,
+                            Contacts._ID + " != " + activity.mQueryAggregateId
+                                    + " AND " + CLAUSE_ONLY_VISIBLE, null,
                             getSortOrder(CONTACTS_SUMMARY_PROJECTION));
                     return;
                 }
@@ -2534,7 +2363,9 @@
 
             // Get the split between starred and frequent items, if the mode is strequent
             mFrequentSeparatorPos = ListView.INVALID_POSITION;
-            if (cursor != null && cursor.getCount() > 0 && mMode == MODE_STREQUENT) {
+            int cursorCount = 0;
+            if (cursor != null && (cursorCount = cursor.getCount()) > 0
+                    && mMode == MODE_STREQUENT) {
                 cursor.move(-1);
                 for (int i = 0; cursor.moveToNext(); i++) {
                     int starred = cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX);
@@ -2549,7 +2380,12 @@
             }
 
             super.changeCursor(cursor);
-
+            if ((mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0) {
+                TextView totalContacts = (TextView) findViewById(R.id.totalContactsText);
+                int stringId = mDisplayOnlyPhones
+                    ? R.string.listTotalPhoneContacts : R.string.listTotalAllContacts;
+                totalContacts.setText(getString(stringId, cursorCount));
+            }
             // Update the indexer for the fast scroll widget
             updateIndexer(cursor);
         }
diff --git a/src/com/android/contacts/ExportVCardActivity.java b/src/com/android/contacts/ExportVCardActivity.java
new file mode 100644
index 0000000..08f43c1
--- /dev/null
+++ b/src/com/android/contacts/ExportVCardActivity.java
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.pim.vcard.VCardComposer;
+import android.provider.ContactsContract.Contacts;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+import java.util.HashSet;
+import java.util.Set;
+
+public class ExportVCardActivity extends Activity {
+    private static final String LOG_TAG = "ExportVCardActivity";
+
+    // If true, VCardExporter is able to emits files longer than 8.3 format.
+    private static final boolean ALLOW_LONG_FILE_NAME = false;
+    private String mTargetDirectory;
+    private String mFileNamePrefix;
+    private String mFileNameSuffix;
+    private int mFileIndexMinimum;
+    private int mFileIndexMaximum;
+    private String mFileNameExtension;
+    private String mVCardTypeStr;
+    private Set<String> mExtensionsToConsider;
+
+    private ProgressDialog mProgressDialog;
+
+    private Handler mHandler = new Handler();
+
+    // Used temporaly when asking users to confirm the file name
+    private String mTargetFileName;
+
+    // String for storing error reason temporaly.
+    private String mErrorReason;
+
+    private ActualExportThread mActualExportThread;
+
+    private class CancelListener
+            implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
+        public void onClick(DialogInterface dialog, int which) {
+            finish();
+        }
+        public void onCancel(DialogInterface dialog) {
+            finish();
+        }
+    }
+
+    private CancelListener mCancelListener = new CancelListener();
+
+    private class ErrorReasonDisplayer implements Runnable {
+        private final int mResId;
+        public ErrorReasonDisplayer(int resId) {
+            mResId = resId;
+        }
+        public ErrorReasonDisplayer(String errorReason) {
+            mResId = R.id.dialog_fail_to_export_with_reason;
+            mErrorReason = errorReason;
+        }
+        public void run() {
+            // Show the Dialog only when the parent Activity is still alive.
+            if (!ExportVCardActivity.this.isFinishing()) {
+                showDialog(mResId);
+            }
+        }
+    }
+
+    private class ExportConfirmationListener implements DialogInterface.OnClickListener {
+        private final String mFileName;
+
+        public ExportConfirmationListener(String fileName) {
+            mFileName = fileName;
+        }
+
+        public void onClick(DialogInterface dialog, int which) {
+            if (which == DialogInterface.BUTTON_POSITIVE) {
+                mActualExportThread = new ActualExportThread(mFileName);
+                String title = getString(R.string.exporting_contact_list_title);
+                String message = getString(R.string.exporting_contact_list_message, mFileName);
+                mProgressDialog = new ProgressDialog(ExportVCardActivity.this);
+                mProgressDialog.setTitle(title);
+                mProgressDialog.setMessage(message);
+                mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+                mProgressDialog.setOnCancelListener(mActualExportThread);
+                mProgressDialog.show();
+                mActualExportThread.start();
+            }
+        }
+    }
+
+    private class ActualExportThread extends Thread
+            implements DialogInterface.OnCancelListener {
+        private PowerManager.WakeLock mWakeLock;
+        private String mFileName;
+        private boolean mCanceled = false;
+
+        public ActualExportThread(String fileName) {
+            mFileName = fileName;
+            PowerManager powerManager = (PowerManager)getSystemService(Context.POWER_SERVICE);
+            mWakeLock = powerManager.newWakeLock(
+                    PowerManager.SCREEN_DIM_WAKE_LOCK |
+                    PowerManager.ON_AFTER_RELEASE, LOG_TAG);
+        }
+
+        @Override
+        public void run() {
+            boolean shouldCallFinish = true;
+            mWakeLock.acquire();
+            VCardComposer composer = null;
+            try {
+                OutputStream outputStream = null;
+                try {
+                    outputStream = new FileOutputStream(mFileName);
+                } catch (FileNotFoundException e) {
+                    final String errorReason =
+                        getString(R.string.fail_reason_could_not_open_file,
+                                mFileName, e.getMessage());
+                    shouldCallFinish = false;
+                    mHandler.post(new ErrorReasonDisplayer(errorReason));
+                    return;
+                }
+
+                composer = new VCardComposer(ExportVCardActivity.this, mVCardTypeStr, true);
+                // composer = new VCardComposer(ExportVCardActivity,
+                // VCardConfig.VCARD_TYPE_V30_JAPANESE_UTF8, true);
+                composer.addHandler(composer.new HandlerForOutputStream(outputStream));
+ 
+                if (!composer.init()) {
+                    final String errorReason = composer.getErrorReason();
+                    Log.e(LOG_TAG, "initialization of vCard composer failed: " + errorReason);
+                    final String translatedErrorReason =
+                            translateComposerError(errorReason);
+                    mHandler.post(new ErrorReasonDisplayer(
+                            getString(R.string.fail_reason_could_not_initialize_exporter,
+                                    translatedErrorReason)));
+                    shouldCallFinish = false;
+                    return;
+                }
+
+                int size = composer.getCount();
+
+                if (size == 0) {
+                    mHandler.post(new ErrorReasonDisplayer(
+                            getString(R.string.fail_reason_no_exportable_contact)));
+                    shouldCallFinish = false;
+                    return;
+                }
+
+                mProgressDialog.setProgressNumberFormat(
+                        getString(R.string.exporting_contact_list_progress));
+                mProgressDialog.setMax(size);
+                mProgressDialog.setProgress(0);
+
+                while (!composer.isAfterLast()) {
+                    if (mCanceled) {
+                        return;
+                    }
+                    if (!composer.createOneEntry()) {
+                        final String errorReason = composer.getErrorReason();
+                        Log.e(LOG_TAG, "Failed to read a contact: " + errorReason);
+                        final String translatedErrorReason =
+                            translateComposerError(errorReason);
+                        mHandler.post(new ErrorReasonDisplayer(
+                                getString(R.string.fail_reason_error_occurred_during_export,
+                                        translatedErrorReason)));
+                        shouldCallFinish = false;
+                        return;
+                    }
+                    mProgressDialog.incrementProgressBy(1);
+                }
+            } finally {
+                if (composer != null) {
+                    composer.terminate();
+                }
+                mWakeLock.release();
+                mProgressDialog.dismiss();
+                if (shouldCallFinish && !isFinishing()) {
+                    finish();
+                }
+            }
+        }
+
+        @Override
+        public void finalize() {
+            if (mWakeLock != null && mWakeLock.isHeld()) {
+                mWakeLock.release();
+            }
+        }
+
+        public void cancel() {
+            mCanceled = true;
+        }
+
+        public void onCancel(DialogInterface dialog) {
+            cancel();
+        }
+    }
+
+    private String translateComposerError(String errorMessage) {
+        Resources resources = getResources();
+        if (VCardComposer.FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO.equals(errorMessage)) {
+            return resources.getString(R.string.composer_failed_to_get_database_infomation);
+        } else if (VCardComposer.FAILURE_REASON_NO_ENTRY.equals(errorMessage)) {
+            return resources.getString(R.string.composer_has_no_exportable_contact);
+        } else if (VCardComposer.FAILURE_REASON_NOT_INITIALIZED.equals(errorMessage)) {
+            return resources.getString(R.string.composer_not_initialized);
+        } else {
+            return errorMessage;
+        }
+    }
+
+    @Override
+    protected void onCreate(Bundle bundle) {
+        super.onCreate(bundle);
+
+        mTargetDirectory = getString(R.string.config_export_dir);
+        mFileNamePrefix = getString(R.string.config_export_file_prefix);
+        mFileNameSuffix = getString(R.string.config_export_file_suffix);
+        mFileNameExtension = getString(R.string.config_export_file_extension);
+        mVCardTypeStr = getString(R.string.config_export_vcard_type);
+
+        mExtensionsToConsider = new HashSet<String>();
+        mExtensionsToConsider.add(mFileNameExtension);
+
+        final String additionalExtensions =
+            getString(R.string.config_export_extensions_to_consider);
+        if (!TextUtils.isEmpty(additionalExtensions)) {
+            for (String extension : additionalExtensions.split(",")) {
+                String trimed = extension.trim();
+                if (trimed.length() > 0) {
+                    mExtensionsToConsider.add(trimed);
+                }
+            }
+        }
+
+        final Resources resources = getResources();
+        mFileIndexMinimum = resources.getInteger(R.integer.config_export_file_min_index);
+        mFileIndexMaximum = resources.getInteger(R.integer.config_export_file_max_index);
+
+        startExportVCardToSdCard();
+    }
+
+    @Override
+    protected Dialog onCreateDialog(int id) {
+        switch (id) {
+            case R.id.dialog_export_confirmation: {
+                return getExportConfirmationDialog();
+            }
+            case R.string.fail_reason_too_many_vcard: {
+                return new AlertDialog.Builder(this)
+                    .setTitle(R.string.exporting_contact_failed_title)
+                    .setMessage(getString(R.string.exporting_contact_failed_message,
+                                getString(R.string.fail_reason_too_many_vcard)))
+                                .setPositiveButton(android.R.string.ok, mCancelListener)
+                                .create();
+            }
+            case R.id.dialog_fail_to_export_with_reason: {
+                return getErrorDialogWithReason();
+            }
+            case R.id.dialog_sdcard_not_found: {
+                AlertDialog.Builder builder = new AlertDialog.Builder(this)
+                .setTitle(R.string.no_sdcard_title)
+                .setIcon(android.R.drawable.ic_dialog_alert)
+                .setMessage(R.string.no_sdcard_message)
+                .setPositiveButton(android.R.string.ok, mCancelListener);
+            }
+        }
+        return super.onCreateDialog(id);
+    }
+
+    @Override
+    protected void onPrepareDialog(int id, Dialog dialog) {
+        if (id == R.id.dialog_fail_to_export_with_reason) {
+            ((AlertDialog)dialog).setMessage(getErrorReason());
+        } else if (id == R.id.dialog_export_confirmation) {
+            ((AlertDialog)dialog).setMessage(
+                    getString(R.string.confirm_export_message, mTargetFileName));
+        } else {
+            super.onPrepareDialog(id, dialog);
+        }
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        if (mActualExportThread != null) {
+            // The Activity is no longer visible. Stop the thread.
+            mActualExportThread.cancel();
+            mActualExportThread = null;
+        }
+
+        if (!isFinishing()) {
+            finish();
+        }
+    }
+
+    /**
+     * Tries to start exporting VCard. If there's no SDCard available,
+     * an error dialog is shown.
+     */
+    public void startExportVCardToSdCard() {
+        File targetDirectory = new File(mTargetDirectory);
+
+        if (!(targetDirectory.exists() &&
+                targetDirectory.isDirectory() &&
+                targetDirectory.canRead()) &&
+                !targetDirectory.mkdirs()) {
+            showDialog(R.id.dialog_sdcard_not_found);
+        } else {
+            mTargetFileName = getAppropriateFileName(mTargetDirectory);
+            if (TextUtils.isEmpty(mTargetFileName)) {
+                mTargetFileName = null;
+                // finish() is called via the error dialog. Do not call the method here.
+                return;
+            }
+
+            showDialog(R.id.dialog_export_confirmation);
+        }
+    }
+
+    /**
+     * Tries to get an appropriate filename. Returns null if it fails.
+     */
+    private String getAppropriateFileName(final String destDirectory) {
+        int fileNumberStringLength = 0;
+        {
+            // Calling Math.Log10() is costly.
+            int tmp;
+            for (fileNumberStringLength = 0, tmp = mFileIndexMaximum; tmp > 0;
+                fileNumberStringLength++, tmp /= 10) {
+            }
+        }
+        String bodyFormat = "%s%0" + fileNumberStringLength + "d%s";
+
+        if (!ALLOW_LONG_FILE_NAME) {
+            String possibleBody = String.format(bodyFormat,mFileNamePrefix, 1, mFileNameSuffix);
+            if (possibleBody.length() > 8 || mFileNameExtension.length() > 3) {
+                Log.e(LOG_TAG, "This code does not allow any long file name.");
+                mErrorReason = getString(R.string.fail_reason_too_long_filename,
+                        String.format("%s.%s", possibleBody, mFileNameExtension));
+                showDialog(R.id.dialog_fail_to_export_with_reason);
+                // finish() is called via the error dialog. Do not call the method here.
+                return null;
+            }
+        }
+
+        // Note that this logic assumes that the target directory is case insensitive.
+        // As of 2009-07-16, it is true since the external storage is only sdcard, and
+        // it is formated as FAT/VFAT.
+        // TODO: fix this.
+        for (int i = mFileIndexMinimum; i <= mFileIndexMaximum; i++) {
+            boolean numberIsAvailable = true;
+            // SD Association's specification seems to require this feature, though we cannot
+            // have the specification since it is proprietary...
+            String body = null;
+            for (String possibleExtension : mExtensionsToConsider) {
+                body = String.format(bodyFormat, mFileNamePrefix, i, mFileNameSuffix);
+                File file = new File(String.format("%s/%s.%s",
+                        destDirectory, body, possibleExtension));
+                if (file.exists()) {
+                    numberIsAvailable = false;
+                    break;
+                }
+            }
+            if (numberIsAvailable) {
+                return String.format("%s/%s.%s", destDirectory, body, mFileNameExtension);
+            }
+        }
+        showDialog(R.string.fail_reason_too_many_vcard);
+        return null;
+    }
+
+    public Dialog getExportConfirmationDialog() {
+        if (TextUtils.isEmpty(mTargetFileName)) {
+            Log.e(LOG_TAG, "Target file name is empty, which must not be!");
+            // This situation is not acceptable (probably a bug!), but we don't have no reason to
+            // show...
+            mErrorReason = null;
+            return getErrorDialogWithReason();
+        }
+
+        return new AlertDialog.Builder(this)
+            .setTitle(R.string.confirm_export_title)
+            .setMessage(getString(R.string.confirm_export_message, mTargetFileName))
+            .setPositiveButton(android.R.string.ok,
+                    new ExportConfirmationListener(mTargetFileName))
+            .setNegativeButton(android.R.string.cancel, mCancelListener)
+            .setOnCancelListener(mCancelListener)
+            .create();
+    }
+
+    public Dialog getErrorDialogWithReason() {
+        if (mErrorReason == null) {
+            Log.e(LOG_TAG, "Error reason must have been set.");
+            mErrorReason = getString(R.string.fail_reason_unknown);
+        }
+        return new AlertDialog.Builder(this)
+            .setTitle(R.string.exporting_contact_failed_title)
+                .setMessage(getString(R.string.exporting_contact_failed_message, mErrorReason))
+            .setPositiveButton(android.R.string.ok, mCancelListener)
+            .setOnCancelListener(mCancelListener)
+            .create();
+    }
+
+    public void cancelExport() {
+        if (mActualExportThread != null) {
+            mActualExportThread.cancel();
+            mActualExportThread = null;
+        }
+    }
+
+    public String getErrorReason() {
+        return mErrorReason;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/ImportVCardActivity.java b/src/com/android/contacts/ImportVCardActivity.java
index e77a3e8..64b5727 100644
--- a/src/com/android/contacts/ImportVCardActivity.java
+++ b/src/com/android/contacts/ImportVCardActivity.java
@@ -531,22 +531,14 @@
             mProgressDialogForScanVCard = null;
 
             if (mGotIOException) {
-                mHandler.post(new Runnable() {
-                    public void run() {
-                        showDialog(R.id.dialog_io_exception);
-                    }
-                });
+                mHandler.post(new DialogDisplayer(R.id.dialog_io_exception));
             } else if (mCanceled) {
                 finish();
             } else {
                 int size = mAllVCardFileList.size();
                 final Context context = ImportVCardActivity.this;
                 if (size == 0) {
-                    mHandler.post(new Runnable() {
-                        public void run() {
-                            showDialog(R.id.dialog_vcard_not_found);
-                        }
-                    });
+                    mHandler.post(new DialogDisplayer(R.id.dialog_vcard_not_found));
                 } else {
                     startVCardSelectAndImport();
                 }
diff --git a/src/com/android/contacts/PhoneDisambigDialog.java b/src/com/android/contacts/PhoneDisambigDialog.java
index 58d3721..b727c77 100644
--- a/src/com/android/contacts/PhoneDisambigDialog.java
+++ b/src/com/android/contacts/PhoneDisambigDialog.java
@@ -17,8 +17,9 @@
 package com.android.contacts;
 
 import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
+import java.util.List;
+
+import com.android.contacts.Collapser.Collapsible;
 
 import android.app.AlertDialog;
 import android.content.ContentUris;
@@ -28,12 +29,13 @@
 import android.database.Cursor;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.telephony.PhoneNumberUtils;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.ArrayAdapter;
 import android.widget.CheckBox;
 import android.widget.CompoundButton;
-import android.widget.SimpleCursorAdapter;
+import android.widget.ListAdapter;
 
 /**
  * Class used for displaying a dialog with a list of phone numbers of which
@@ -47,6 +49,8 @@
     private AlertDialog mDialog;
     private boolean mSendSms;
     private Cursor mPhonesCursor;
+    private ListAdapter mPhonesAdapter;
+    private ArrayList<PhoneItem> mPhoneItemList;
 
     public PhoneDisambigDialog(Context context, Cursor phonesCursor) {
         this(context, phonesCursor, false /*make call*/);
@@ -57,6 +61,11 @@
         mSendSms = sendSms;
         mPhonesCursor = phonesCursor;
 
+        mPhoneItemList = makePhoneItemsList(phonesCursor);
+        Collapser.collapseList(mPhoneItemList);
+
+        mPhonesAdapter = new PhonesAdapter(mContext, mPhoneItemList);
+
         LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
                 Context.LAYOUT_INFLATER_SERVICE);
         View setPrimaryView = inflater.
@@ -66,9 +75,10 @@
 
         // Need to show disambig dialogue.
         AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mContext).
-            setCursor(mPhonesCursor, this, Phone.NUMBER).
-                    setTitle(sendSms ? R.string.sms_disambig_title : R.string.call_disambig_title).
-                    setView(setPrimaryView);
+                setAdapter(mPhonesAdapter, this).
+                        setTitle(sendSms ?
+                                R.string.sms_disambig_title : R.string.call_disambig_title).
+                        setView(setPrimaryView);
 
         mDialog = dialogBuilder.create();
     }
@@ -77,18 +87,25 @@
      * Show the dialog.
      */
     public void show() {
+        if (mPhoneItemList.size() == 1) {
+            // If there is only one after collapse, just select it, and close;
+            onClick(mDialog, 0);
+            return;
+        }
         mDialog.show();
     }
 
     public void onClick(DialogInterface dialog, int which) {
-        if (mPhonesCursor.moveToPosition(which)) {
-            long id = mPhonesCursor.getLong(mPhonesCursor.getColumnIndex(Data._ID));
-            String phone = mPhonesCursor.getString(mPhonesCursor.getColumnIndex(Phone.NUMBER));
+        if (mPhoneItemList.size() > which && which >= 0) {
+            PhoneItem phoneItem = mPhoneItemList.get(which);
+            long id = phoneItem.id;
+            String phone = phoneItem.phoneNumber;
+
             if (mMakePrimary) {
                 ContentValues values = new ContentValues(1);
                 values.put(Data.IS_SUPER_PRIMARY, 1);
-                mContext.getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, id),
-                        values, null, null);
+                mContext.getContentResolver().update(ContentUris.
+                        withAppendedId(Data.CONTENT_URI, id), values, null, null);
             }
 
             if (mSendSms) {
@@ -108,4 +125,56 @@
     public void onDismiss(DialogInterface dialog) {
         mPhonesCursor.close();
     }
+
+    private static class PhonesAdapter extends ArrayAdapter<PhoneItem> {
+
+        public PhonesAdapter(Context context, List<PhoneItem> objects) {
+            super(context, android.R.layout.simple_dropdown_item_1line,
+                    android.R.id.text1, objects);
+        }
+    }
+
+    private class PhoneItem implements Collapsible<PhoneItem> {
+
+        String phoneNumber;
+        long id;
+
+        public PhoneItem(String newPhoneNumber, long newId) {
+            phoneNumber = newPhoneNumber;
+            id = newId;
+        }
+
+        public boolean collapseWith(PhoneItem phoneItem) {
+            if (!shouldCollapseWith(phoneItem)) {
+                return false;
+            }
+            // Just keep the number and id we already have.
+            return true;
+        }
+
+        public boolean shouldCollapseWith(PhoneItem phoneItem) {
+            if (PhoneNumberUtils.compare(PhoneDisambigDialog.this.mContext,
+                    phoneNumber, phoneItem.phoneNumber)) {
+                return true;
+            }
+            return false;
+        }
+
+        public String toString() {
+            return phoneNumber;
+        }
+    }
+
+    private ArrayList<PhoneItem> makePhoneItemsList(Cursor phonesCursor) {
+        ArrayList<PhoneItem> phoneList = new ArrayList<PhoneItem>();
+
+        phonesCursor.moveToPosition(-1);
+        while (phonesCursor.moveToNext()) {
+            long id = phonesCursor.getLong(phonesCursor.getColumnIndex(Data._ID));
+            String phone = phonesCursor.getString(phonesCursor.getColumnIndex(Phone.NUMBER));
+            phoneList.add(new PhoneItem(phone, id));
+        }
+
+        return phoneList;
+    }
 }
diff --git a/src/com/android/contacts/VCardExporter.java b/src/com/android/contacts/VCardExporter.java
deleted file mode 100644
index 5f13e99..0000000
--- a/src/com/android/contacts/VCardExporter.java
+++ /dev/null
@@ -1,325 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.contacts;
-
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.app.ProgressDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.res.Resources;
-import android.os.Handler;
-import android.os.PowerManager;
-import android.pim.vcard.VCardComposer;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.OutputStream;
-import java.util.HashSet;
-import java.util.Set;
-
-public class VCardExporter {
-    private static final String LOG_TAG = "VCardExporter";
-
-    // If true, VCardExporter is able to emits files longer than 8.3 format.
-    private static final boolean ALLOW_LONG_FILE_NAME = false;
-    private final String mTargetDirectory;
-    private final String mFileNamePrefix;
-    private final String mFileNameSuffix;
-    private final int mFileIndexMinimum;
-    private final int mFileIndexMaximum;
-    private final String mFileNameExtension;
-    private final String mVCardTypeStr;
-    private final Set<String> mExtensionsToConsider;
-
-    private ContactsListActivity mParentActivity;
-    private ProgressDialog mProgressDialog;
-
-    // Used temporaly when asking users to confirm the file name
-    private String mTargetFileName;
-
-    // String for storing error reason temporaly.
-    private String mErrorReason;
-
-    private ActualExportThread mActualExportThread;
-
-    private class ConfirmListener implements DialogInterface.OnClickListener {
-        private String mFileName;
-
-        public ConfirmListener(String fileName) {
-            mFileName = fileName;
-        }
-
-        public void onClick(DialogInterface dialog, int which) {
-            if (which == DialogInterface.BUTTON_POSITIVE) {
-                mActualExportThread = new ActualExportThread(mFileName);
-                String title = getString(R.string.exporting_contact_list_title);
-                String message = getString(R.string.exporting_contact_list_message, mFileName);
-                mProgressDialog = new ProgressDialog(mParentActivity);
-                mProgressDialog.setTitle(title);
-                mProgressDialog.setMessage(message);
-                mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
-                mProgressDialog.setOnCancelListener(mActualExportThread);
-                mParentActivity.showDialog(R.id.dialog_exporting_vcard);
-                mActualExportThread.start();
-            }
-        }
-    }
-
-    private class ActualExportThread extends Thread
-            implements DialogInterface.OnCancelListener {
-        private PowerManager.WakeLock mWakeLock;
-        private String mFileName;
-        private boolean mCanceled = false;
-
-        public ActualExportThread(String fileName) {
-            mFileName = fileName;
-            PowerManager powerManager = (PowerManager)mParentActivity.getSystemService(
-                    Context.POWER_SERVICE);
-            mWakeLock = powerManager.newWakeLock(
-                    PowerManager.SCREEN_DIM_WAKE_LOCK |
-                    PowerManager.ON_AFTER_RELEASE, LOG_TAG);
-        }
-
-        @Override
-        public void run() {
-            mWakeLock.acquire();
-            VCardComposer composer = null;
-            try {
-                OutputStream outputStream = null;
-                try {
-                    outputStream = new FileOutputStream(mFileName);
-                } catch (FileNotFoundException e) {
-                    mErrorReason = getString(R.string.fail_reason_could_not_open_file,
-                            mFileName, e.getMessage());
-                    mParentActivity.showDialog(R.id.dialog_fail_to_export_with_reason);
-                    return;
-                }
-
-                composer = new VCardComposer(mParentActivity, mVCardTypeStr, true);
-                // composer = new VCardComposer(mParentContext,
-                // VCardConfig.VCARD_TYPE_V30_JAPANESE_UTF8, true);
-                composer.addHandler(composer.new HandlerForOutputStream(outputStream));
-
-                if (!composer.init()) {
-                    mErrorReason = getString(R.string.fail_reason_could_not_initialize_exporter,
-                            composer.getErrorReason());
-                    mParentActivity.showDialog(R.id.dialog_fail_to_export_with_reason);
-                    return;
-                }
-
-                int size = composer.getCount();
-
-                mProgressDialog.setProgressNumberFormat(
-                        getString(R.string.exporting_contact_list_progress));
-                mProgressDialog.setMax(size);
-                mProgressDialog.setProgress(0);
-
-                while (!composer.isAfterLast()) {
-                    if (mCanceled) {
-                        return;
-                    }
-                    if (!composer.createOneEntry()) {
-                        Log.e(LOG_TAG, "Failed to read a contact.");
-                        mErrorReason = getString(R.string.fail_reason_error_occurred_during_export,
-                                composer.getErrorReason());
-                        mParentActivity.showDialog(R.id.dialog_fail_to_export_with_reason);
-                        return;
-                    }
-                    mProgressDialog.incrementProgressBy(1);
-                }
-            } finally {
-                if (composer != null) {
-                    composer.terminate();
-                }
-                mWakeLock.release();
-                mProgressDialog.dismiss();
-                mParentActivity.removeReferenceToVCardExporter();
-            }
-        }
-
-        @Override
-        public void finalize() {
-            if (mWakeLock != null && mWakeLock.isHeld()) {
-                mWakeLock.release();
-            }
-        }
-
-        public void cancel() {
-            mCanceled = true;
-        }
-
-        public void onCancel(DialogInterface dialog) {
-            cancel();
-        }
-    }
-
-    /**
-     * @param parentActivity must not be null
-     */
-    public VCardExporter(ContactsListActivity parentActivity) {
-        mParentActivity = parentActivity;
-        mTargetDirectory = getString(R.string.config_export_dir);
-        mFileNamePrefix = getString(R.string.config_export_file_prefix);
-        mFileNameSuffix = getString(R.string.config_export_file_suffix);
-        mFileNameExtension = getString(R.string.config_export_file_extension);
-        mVCardTypeStr = getString(R.string.config_export_vcard_type);
-
-        mExtensionsToConsider = new HashSet<String>();
-        mExtensionsToConsider.add(mFileNameExtension);
-
-        final String additionalExtensions =
-            getString(R.string.config_export_extensions_to_consider);
-        if (!TextUtils.isEmpty(additionalExtensions)) {
-            for (String extension : additionalExtensions.split(",")) {
-                String trimed = extension.trim();
-                if (trimed.length() > 0) {
-                    mExtensionsToConsider.add(trimed);
-                }
-            }
-        }
-
-        Resources resources = parentActivity.getResources();
-        mFileIndexMinimum = resources.getInteger(R.integer.config_export_file_min_index);
-        mFileIndexMaximum = resources.getInteger(R.integer.config_export_file_max_index);
-    }
-
-    /**
-     * Tries to start exporting VCard. If there's no SDCard available,
-     * an error dialog is shown.
-     */
-    public void startExportVCardToSdCard() {
-        File targetDirectory = new File(mTargetDirectory);
-
-        if (!(targetDirectory.exists() &&
-                targetDirectory.isDirectory() &&
-                targetDirectory.canRead()) &&
-                !targetDirectory.mkdirs()) {
-            mParentActivity.showDialog(R.id.dialog_sdcard_not_found);
-        } else {
-            mTargetFileName = getAppropriateFileName(mTargetDirectory);
-            if (TextUtils.isEmpty(mTargetFileName)) {
-                mTargetFileName = null;
-                return;
-            }
-
-            mParentActivity.showDialog(R.id.dialog_confirm_export_vcard);
-        }
-    }
-
-    /**
-     * Tries to get an appropriate filename. Returns null if it fails.
-     */
-    private String getAppropriateFileName(final String destDirectory) {
-        int fileNumberStringLength = 0;
-        {
-            // Calling Math.Log10() is costly.
-            int tmp;
-            for (fileNumberStringLength = 0, tmp = mFileIndexMaximum; tmp > 0;
-                fileNumberStringLength++, tmp /= 10) {
-            }
-        }
-        String bodyFormat = "%s%0" + fileNumberStringLength + "d%s";
-
-        if (!ALLOW_LONG_FILE_NAME) {
-            String possibleBody = String.format(bodyFormat,mFileNamePrefix, 1, mFileNameSuffix);
-            if (possibleBody.length() > 8 || mFileNameExtension.length() > 3) {
-                Log.e(LOG_TAG, "This code does not allow any long file name.");
-                mErrorReason = getString(R.string.fail_reason_too_long_filename,
-                        String.format("%s.%s", possibleBody, mFileNameExtension));
-                mParentActivity.showDialog(R.id.dialog_fail_to_export_with_reason);
-                return null;
-            }
-        }
-
-        // Note that this logic assumes that the target directory is case insensitive.
-        // As of 2009-07-16, it is true since the external storage is only sdcard, and
-        // it is formated as FAT/VFAT.
-        // TODO: fix this.
-        for (int i = mFileIndexMinimum; i <= mFileIndexMaximum; i++) {
-            boolean numberIsAvailable = true;
-            // SD Association's specification seems to require this feature, though we cannot
-            // have the specification since it is proprietary...
-            String body = null;
-            for (String possibleExtension : mExtensionsToConsider) {
-                body = String.format(bodyFormat, mFileNamePrefix, i, mFileNameSuffix);
-                File file = new File(String.format("%s/%s.%s",
-                        destDirectory, body, possibleExtension));
-                if (file.exists()) {
-                    numberIsAvailable = false;
-                    break;
-                }
-            }
-            if (numberIsAvailable) {
-                return String.format("%s/%s.%s", destDirectory, body, mFileNameExtension);
-            }
-        }
-        mParentActivity.showDialog(R.string.fail_reason_too_many_vcard);
-        return null;
-    }
-
-    public Dialog getExportConfirmationDialog() {
-        if (TextUtils.isEmpty(mTargetFileName)) {
-            Log.e(LOG_TAG, "Target file name is empty, which must not be!");
-            // This situation is not acceptable (probably a bug!), but we don't have no reason to
-            // show...
-            mErrorReason = null;
-            return getErrorDialogWithReason();
-        }
-
-        return new AlertDialog.Builder(mParentActivity)
-            .setTitle(R.string.confirm_export_title)
-            .setMessage(getString(R.string.confirm_export_message, mTargetFileName))
-            .setPositiveButton(android.R.string.ok, new ConfirmListener(mTargetFileName))
-            .setNegativeButton(android.R.string.cancel, null)
-            .create();
-    }
-
-    public Dialog getExportingVCardDialog() {
-        return mProgressDialog;
-    }
-
-    public Dialog getErrorDialogWithReason() {
-        if (mErrorReason == null) {
-            Log.e(LOG_TAG, "Error reason must have been set.");
-            mErrorReason = getString(R.string.fail_reason_unknown);
-        }
-        return new AlertDialog.Builder(mParentActivity)
-            .setTitle(R.string.exporting_contact_failed_title)
-                .setMessage(getString(R.string.exporting_contact_failed_message, mErrorReason))
-            .setPositiveButton(android.R.string.ok, null)
-            .create();
-    }
-
-    public void cancelExport() {
-        if (mActualExportThread != null) {
-            mActualExportThread.cancel();
-            mActualExportThread = null;
-        }
-    }
-
-    private String getString(int resId, Object... formatArgs) {
-        return mParentActivity.getString(resId, formatArgs);
-    }
-
-    private String getString(int resId) {
-        return mParentActivity.getString(resId);
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/contacts/ViewContactActivity.java b/src/com/android/contacts/ViewContactActivity.java
index c1fab89..b1910d6 100644
--- a/src/com/android/contacts/ViewContactActivity.java
+++ b/src/com/android/contacts/ViewContactActivity.java
@@ -74,13 +74,11 @@
 import android.view.Window;
 import android.view.ContextMenu.ContextMenuInfo;
 import android.view.animation.Animation;
-import android.view.animation.AnimationUtils;
 import android.view.animation.TranslateAnimation;
 import android.view.animation.Animation.AnimationListener;
 import android.widget.AdapterView;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
-import android.widget.LinearLayout;
 import android.widget.ListView;
 import android.widget.TextView;
 import android.widget.Toast;
@@ -137,6 +135,7 @@
     protected ScrollingTabWidget mTabWidget;
     protected ContactHeaderWidget mContactHeaderWidget;
     protected View mBelowHeader;
+    protected TextView mAccountName;
     protected View mBufferView;
     private NotifyingAsyncQueryHandler mHandler;
 
@@ -218,6 +217,7 @@
         mTabsVisible = false;
 
         mBelowHeader = findViewById(R.id.below_header);
+        mAccountName = (TextView) findViewById(R.id.account_name);
 
         mTabRawContactIdMap = new SparseArray<Long>();
 
@@ -608,6 +608,7 @@
         }
 
         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);
@@ -669,9 +670,13 @@
                 return true;
             }
             case R.id.menu_share: {
+                // TODO: Keep around actual LOOKUP_KEY, or formalize method of extracting
+                final String lookupKey = mLookupUri.getPathSegments().get(2);
+                final Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey);
+
                 final Intent intent = new Intent(Intent.ACTION_SEND);
-                intent.setType(Contacts.CONTENT_ITEM_TYPE);
-                intent.putExtra(Intent.EXTRA_STREAM, mLookupUri);
+                intent.setType(Contacts.CONTENT_VCARD_TYPE);
+                intent.putExtra(Intent.EXTRA_STREAM, shareUri);
 
                 // Launch chooser to share contact via
                 final CharSequence chooseTitle = getText(R.string.share_via);
@@ -940,6 +945,14 @@
                     continue;
                 }
 
+                if (mTabsVisible) {
+                    final String accountName = entValues.getAsString(RawContacts.ACCOUNT_NAME);
+                    mAccountName.setText(getString(R.string.account_name_format, accountName));
+                    mAccountName.setVisibility(View.VISIBLE);
+                } else {
+                    mAccountName.setVisibility(View.GONE);
+                }
+
                 for (NamedContentValues subValue : entity.getSubValues()) {
                     ViewEntry entry = new ViewEntry();
 
@@ -1141,7 +1154,7 @@
     /**
      * A basic structure with the data for a contact entry in the list.
      */
-    static class ViewEntry extends ContactEntryAdapter.Entry implements Collapsible<ViewEntry> {
+    class ViewEntry extends ContactEntryAdapter.Entry implements Collapsible<ViewEntry> {
         public String resPackageName = null;
         public int actionIcon = -1;
         public boolean isPrimary = false;
@@ -1156,7 +1169,7 @@
 
         public boolean collapseWith(ViewEntry entry) {
             // assert equal collapse keys
-            if (!getCollapseKey().equals(entry.getCollapseKey())) {
+            if (!shouldCollapseWith(entry)) {
                 return false;
             }
 
@@ -1188,16 +1201,43 @@
             return true;
         }
 
-        public String getCollapseKey() {
-            StringBuilder hashSb = new StringBuilder();
-            hashSb.append(data);
-            hashSb.append(mimetype);
-            hashSb.append((intent != null && intent.getAction() != null)
-                    ? intent.getAction() : "");
-            hashSb.append((secondaryIntent != null && secondaryIntent.getAction() != null)
-                    ? secondaryIntent.getAction() : "");
-            hashSb.append(actionIcon);
-            return hashSb.toString();
+        public boolean shouldCollapseWith(ViewEntry entry) {
+            if (entry == null) {
+                return false;
+            }
+
+            if (Phone.CONTENT_ITEM_TYPE.equals(mimetype)
+                    && Phone.CONTENT_ITEM_TYPE.equals(entry.mimetype)) {
+                if (!PhoneNumberUtils.compare(ViewContactActivity.this, data, entry.data)) {
+                    return false;
+                }
+            } else {
+                if (!equals(data, entry.data)) {
+                    return false;
+                }
+            }
+
+            if (!equals(mimetype, entry.mimetype)
+                    || !intentCollapsible(intent, entry.intent)
+                    || !intentCollapsible(secondaryIntent, entry.secondaryIntent)
+                    || actionIcon != entry.actionIcon) {
+                return false;
+            }
+
+            return true;
+        }
+
+        private boolean equals(Object a, Object b) {
+            return a==b || (a != null && a.equals(b));
+        }
+
+        private boolean intentCollapsible(Intent a, Intent b) {
+            if (a == b) {
+                return true;
+            } else if ((a != null && b != null) && equals(a.getAction(), b.getAction())) {
+                return true;
+            }
+            return false;
         }
     }
 
diff --git a/src/com/android/contacts/model/Editor.java b/src/com/android/contacts/model/Editor.java
index d6f7003..b7ae045 100644
--- a/src/com/android/contacts/model/Editor.java
+++ b/src/com/android/contacts/model/Editor.java
@@ -49,7 +49,7 @@
      * builds any needed views. Any changes performed by the user will be
      * written back to that same object.
      */
-    public void setValues(DataKind kind, ValuesDelta values, EntityDelta state);
+    public void setValues(DataKind kind, ValuesDelta values, EntityDelta state, boolean readOnly);
 
     /**
      * Add a specific {@link EditorListener} to this {@link Editor}.
diff --git a/src/com/android/contacts/model/EntityDelta.java b/src/com/android/contacts/model/EntityDelta.java
index f95bc12..7ac0f5b 100644
--- a/src/com/android/contacts/model/EntityDelta.java
+++ b/src/com/android/contacts/model/EntityDelta.java
@@ -437,6 +437,7 @@
         protected ContentValues mBefore;
         protected ContentValues mAfter;
         protected String mIdColumn = BaseColumns._ID;
+        private boolean mFromTemplate;
 
         /**
          * Next value to assign to {@link #mIdColumn} when building an insert
@@ -543,6 +544,14 @@
             return isPrimary == null ? false : isPrimary != 0;
         }
 
+        public void setFromTemplate(boolean isFromTemplate) {
+            mFromTemplate = isFromTemplate;
+        }
+
+        public boolean isFromTemplate() {
+            return mFromTemplate;
+        }
+
         public boolean beforeExists() {
             return (mBefore != null && mBefore.containsKey(mIdColumn));
         }
diff --git a/src/com/android/contacts/model/EntityModifier.java b/src/com/android/contacts/model/EntityModifier.java
index beaf3e6..d6f6571 100644
--- a/src/com/android/contacts/model/EntityModifier.java
+++ b/src/com/android/contacts/model/EntityModifier.java
@@ -389,7 +389,7 @@
                     // TODO: remove this verbose logging
                     Log.w(TAG, "Trimming: " + entry.toString());
                     entry.markDeleted();
-                } else {
+                } else if (!entry.isFromTemplate()) {
                     hasValues = true;
                 }
             }
diff --git a/src/com/android/contacts/model/ExchangeSource.java b/src/com/android/contacts/model/ExchangeSource.java
index 0a7fb23..6d4e357 100644
--- a/src/com/android/contacts/model/ExchangeSource.java
+++ b/src/com/android/contacts/model/ExchangeSource.java
@@ -125,8 +125,6 @@
                     .add(buildPhoneType(Phone.TYPE_RADIO).setSecondary(true).setSpecificMax(1));
             kind.typeList.add(buildPhoneType(Phone.TYPE_ASSISTANT).setSecondary(true)
                     .setSpecificMax(1).setCustomColumn(Phone.LABEL));
-            kind.typeList.add(buildPhoneType(Phone.TYPE_CUSTOM).setSecondary(true)
-                    .setSpecificMax(1).setCustomColumn(Phone.LABEL));
 
             kind.fieldList = Lists.newArrayList();
             kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
diff --git a/src/com/android/contacts/model/GoogleSource.java b/src/com/android/contacts/model/GoogleSource.java
index 9968ace..b6f1e29 100644
--- a/src/com/android/contacts/model/GoogleSource.java
+++ b/src/com/android/contacts/model/GoogleSource.java
@@ -42,7 +42,6 @@
 
 public class GoogleSource extends FallbackSource {
     public static final String ACCOUNT_TYPE = "com.google.GAIA";
-
     public GoogleSource(String resPackageName) {
         this.accountType = ACCOUNT_TYPE;
         this.resPackageName = null;
@@ -160,6 +159,7 @@
 
     public static final void attemptMyContactsMembership(EntityDelta state, Context context) {
         final ValuesDelta stateValues = state.getValues();
+	stateValues.setFromTemplate(true);
         final String accountName = stateValues.getAsString(RawContacts.ACCOUNT_NAME);
         final String accountType = stateValues.getAsString(RawContacts.ACCOUNT_TYPE);
         attemptMyContactsMembership(state, accountName, accountType, context, true);
diff --git a/src/com/android/contacts/model/Sources.java b/src/com/android/contacts/model/Sources.java
index 04fa493..7d06cab 100644
--- a/src/com/android/contacts/model/Sources.java
+++ b/src/com/android/contacts/model/Sources.java
@@ -24,6 +24,7 @@
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.accounts.AuthenticatorDescription;
+import android.accounts.OnAccountsUpdatedListener;
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -46,10 +47,11 @@
  * Singleton holder for all parsed {@link ContactsSource} available on the
  * system, typically filled through {@link PackageManager} queries.
  */
-public class Sources extends BroadcastReceiver {
+public class Sources extends BroadcastReceiver implements OnAccountsUpdatedListener {
     private static final String TAG = "Sources";
 
-    private Context mContext;
+    private Context mApplicationContext;
+    private AccountManager mAccountManager;
 
     private ContactsSource mFallbackSource = null;
 
@@ -76,13 +78,22 @@
      * Internal constructor that only performs initial parsing.
      */
     private Sources(Context context) {
-        mContext = context;
+        mApplicationContext = context.getApplicationContext();
+        mAccountManager = AccountManager.get(mApplicationContext);
 
         // Create fallback contacts source for on-phone contacts
         mFallbackSource = new FallbackSource();
 
-        loadAccounts();
-        registerIntentReceivers(context);
+        queryAccounts();
+
+        // Request updates when packages or accounts change
+        final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
+        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+        filter.addDataScheme("package");
+
+        mApplicationContext.registerReceiver(this, filter);
+        mAccountManager.addOnAccountsUpdatedListener(this, null, false);
     }
 
     /** @hide exposed for unit tests */
@@ -97,49 +108,50 @@
         mKnownPackages.add(source.resPackageName);
     }
 
-    private void registerIntentReceivers(Context context) {
-        IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
-        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
-        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
-        filter.addDataScheme("package");
-
-        // We use getApplicationContext() so that the broadcast reciever can stay registered for
-        // the length of the application lifetime (instead of the calling activity's lifetime).
-        // This is so that we can notified of package changes, and purge the cache accordingly,
-        // but not be woken up if the application process isn't already running, since we will
-        // have no cache to clear at that point.
-        context.getApplicationContext().registerReceiver(this, filter);
-    }
-
     /** {@inheritDoc} */
     @Override
     public void onReceive(Context context, Intent intent) {
         final String action = intent.getAction();
         final String packageName = intent.getData().getSchemeSpecificPart();
 
-        final boolean matchingPackage = mKnownPackages.contains(packageName);
-        final boolean validAction = Intent.ACTION_PACKAGE_REMOVED.equals(action)
+        if (Intent.ACTION_PACKAGE_REMOVED.equals(action)
                 || Intent.ACTION_PACKAGE_ADDED.equals(action)
-                || Intent.ACTION_PACKAGE_CHANGED.equals(action);
-
-        if (matchingPackage && validAction) {
-            for (ContactsSource source : mSources.values()) {
-                if (TextUtils.equals(packageName, source.resPackageName)) {
-                    // Invalidate any cache for the changed package
-                    source.invalidateCache();
-                }
+                || Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
+            final boolean knownPackage = mKnownPackages.contains(packageName);
+            if (knownPackage) {
+                // Invalidate cache of existing source
+                invalidateCache(packageName);
+            } else {
+                // Unknown source, so reload from scratch
+                queryAccounts();
             }
         }
     }
 
+    protected void invalidateCache(String packageName) {
+        for (ContactsSource source : mSources.values()) {
+            if (TextUtils.equals(packageName, source.resPackageName)) {
+                // Invalidate any cache for the changed package
+                source.invalidateCache();
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    public void onAccountsUpdated(Account[] accounts) {
+        // Refresh to catch any changed accounts
+        queryAccounts();
+    }
+
     /**
      * Blocking call to load all {@link AuthenticatorDescription} known by the
      * {@link AccountManager} on the system.
      */
-    protected void loadAccounts() {
+    protected synchronized void queryAccounts() {
         mSources.clear();
+        mKnownPackages.clear();
 
-        final AccountManager am = AccountManager.get(mContext);
+        final AccountManager am = mAccountManager;
         final IContentService cs = ContentResolver.getContentService();
 
         try {
@@ -200,7 +212,7 @@
      * returned may require inflation before they can be used.
      */
     public ArrayList<Account> getAccounts(boolean writableOnly) {
-        final AccountManager am = AccountManager.get(mContext);
+        final AccountManager am = mAccountManager;
         final Account[] accounts = am.getAccounts();
         final ArrayList<Account> matching = Lists.newArrayList();
 
@@ -259,7 +271,7 @@
             return source;
         } else {
             // Not inflated, but requested that we force-inflate
-            source.ensureInflated(mContext, inflateLevel);
+            source.ensureInflated(mApplicationContext, inflateLevel);
             return source;
         }
     }
diff --git a/src/com/android/contacts/ui/EditContactActivity.java b/src/com/android/contacts/ui/EditContactActivity.java
index 51b10c5..14f80b9 100644
--- a/src/com/android/contacts/ui/EditContactActivity.java
+++ b/src/com/android/contacts/ui/EditContactActivity.java
@@ -97,6 +97,7 @@
 
     private ScrollingTabWidget mTabWidget;
     private ContactHeaderWidget mHeader;
+    private TextView mAccountName;
 
     private ContactEditorView mEditor;
 
@@ -120,11 +121,12 @@
         // Header bar is filled later after queries finish
         mHeader = (ContactHeaderWidget)this.findViewById(R.id.contact_header_widget);
         mHeader.setContactHeaderListener(this);
-        mHeader.showStar(true);
+        mHeader.showStar(false);
         mHeader.enableClickListeners();
 
         mTabWidget = (ScrollingTabWidget)this.findViewById(R.id.tab_widget);
         mTabWidget.setTabSelectionListener(this);
+        mAccountName = (TextView)this.findViewById(R.id.account_name);
 
         // Build editor and listen for photo requests
         mEditor = (ContactEditorView)this.findViewById(android.R.id.tabcontent);
@@ -394,10 +396,15 @@
         if (entity == null) return;
 
         final Sources sources = Sources.getInstance(this);
-        final String accountType = entity.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+        final ValuesDelta values = entity.getValues();
+        final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
+        final String accountName = values.getAsString(RawContacts.ACCOUNT_NAME);
         final ContactsSource source = sources.getInflatedSource(accountType,
                 ContactsSource.LEVEL_CONSTRAINTS);
 
+        mAccountName.setText(getString(R.string.account_name_format, accountName));
+        mAccountName.setVisibility(View.VISIBLE);
+
         // Assign editor state based on entity and source
         mEditor.setState(entity, source);
     }
diff --git a/src/com/android/contacts/ui/widget/ContactEditorView.java b/src/com/android/contacts/ui/widget/ContactEditorView.java
index 35a32cf..cd94e52 100644
--- a/src/com/android/contacts/ui/widget/ContactEditorView.java
+++ b/src/com/android/contacts/ui/widget/ContactEditorView.java
@@ -173,6 +173,8 @@
         EntityModifier.ensureKindExists(state, source, Photo.CONTENT_ITEM_TYPE);
         mHasPhotoEditor = (source.getKindForMimetype(Photo.CONTENT_ITEM_TYPE) != null);
         mPhoto.setVisibility(mHasPhotoEditor ? View.VISIBLE : View.GONE);
+	mPhoto.setEnabled(!source.readOnly);
+	mName.setEnabled(!source.readOnly);
 
         mReadOnly.setVisibility(source.readOnly ? View.VISIBLE : View.GONE);
 
@@ -185,18 +187,18 @@
             if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
                 // Handle special case editor for structured name
                 final ValuesDelta primary = state.getPrimaryEntry(mimeType);
-                mName.setValues(kind, primary, state);
+                mName.setValues(kind, primary, state, source.readOnly);
             } else if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
                 // Handle special case editor for photos
                 final ValuesDelta primary = state.getPrimaryEntry(mimeType);
-                mPhoto.setValues(kind, primary, state);
+                mPhoto.setValues(kind, primary, state, source.readOnly);
             } else {
                 // Otherwise use generic section-based editors
                 if (kind.fieldList == null) continue;
                 final ViewGroup parent = kind.secondary ? mSecondary : mGeneral;
                 final KindSectionView section = (KindSectionView)mInflater.inflate(
                         R.layout.item_kind_section, parent, false);
-                section.setState(kind, state);
+                section.setState(kind, state, source.readOnly);
                 section.setId(kind.weight);
                 parent.addView(section);
             }
diff --git a/src/com/android/contacts/ui/widget/GenericEditorView.java b/src/com/android/contacts/ui/widget/GenericEditorView.java
index 53dd2e0..6387374 100644
--- a/src/com/android/contacts/ui/widget/GenericEditorView.java
+++ b/src/com/android/contacts/ui/widget/GenericEditorView.java
@@ -70,6 +70,7 @@
     protected DataKind mKind;
     protected ValuesDelta mEntry;
     protected EntityDelta mState;
+    protected boolean mReadOnly;
 
     protected boolean mHideOptional = true;
 
@@ -113,6 +114,16 @@
         mDelete.setVisibility(deletable ? View.VISIBLE : View.INVISIBLE);
     }
 
+    public void setEnabled(boolean enabled) {
+        mLabel.setEnabled(enabled);
+	final int count = mFields.getChildCount();
+        for (int pos = 0; pos < count; pos++) {
+            final View v = mFields.getChildAt(pos);
+            v.setEnabled(enabled);
+        }
+        mMore.setEnabled(enabled);
+    }
+
     /**
      * Build the current label state based on selected {@link EditType} and
      * possible custom label string.
@@ -144,17 +155,20 @@
     }
 
     private void rebuildValues() {
-        setValues(mKind, mEntry, mState);
+        setValues(mKind, mEntry, mState, mReadOnly);
     }
 
     /**
      * Prepare this editor using the given {@link DataKind} for defining
      * structure and {@link ValuesDelta} describing the content to edit.
      */
-    public void setValues(DataKind kind, ValuesDelta entry, EntityDelta state) {
+    public void setValues(DataKind kind, ValuesDelta entry, EntityDelta state, boolean readOnly) {
         mKind = kind;
         mEntry = entry;
         mState = state;
+	mReadOnly = readOnly;
+
+	final boolean enabled = !readOnly;
 
         if (!entry.isVisible()) {
             // Hide ourselves entirely if deleted
@@ -167,6 +181,7 @@
         // Display label selector if multiple types available
         final boolean hasTypes = EntityModifier.hasEditTypes(kind);
         mLabel.setVisibility(hasTypes ? View.VISIBLE : View.GONE);
+	mLabel.setEnabled(enabled);
         if (hasTypes) {
             mType = EntityModifier.getCurrentType(entry, kind);
             rebuildLabel();
@@ -207,6 +222,7 @@
             final boolean couldHide = (TextUtils.isEmpty(value) && field.optional);
             final boolean willHide = (mHideOptional && couldHide);
             fieldView.setVisibility(willHide ? View.GONE : View.VISIBLE);
+	    fieldView.setEnabled(enabled);
             hidePossible = hidePossible || couldHide;
 
             mFields.addView(fieldView);
@@ -214,6 +230,7 @@
 
         // When hiding fields, place expandable
         mMore.setVisibility(hidePossible ? View.VISIBLE : View.GONE);
+	mMore.setEnabled(enabled);
     }
 
     /**
diff --git a/src/com/android/contacts/ui/widget/KindSectionView.java b/src/com/android/contacts/ui/widget/KindSectionView.java
index 5a63992..b52cfd0 100644
--- a/src/com/android/contacts/ui/widget/KindSectionView.java
+++ b/src/com/android/contacts/ui/widget/KindSectionView.java
@@ -50,6 +50,7 @@
 
     private DataKind mKind;
     private EntityDelta mState;
+    private boolean mReadOnly;
 
     public KindSectionView(Context context) {
         super(context);
@@ -87,9 +88,10 @@
         // Ignore requests
     }
 
-    public void setState(DataKind kind, EntityDelta state) {
+    public void setState(DataKind kind, EntityDelta state, boolean readOnly) {
         mKind = kind;
         mState = state;
+        mReadOnly = readOnly;
 
         // TODO: handle resources from remote packages
         mTitle.setText(kind.titleRes);
@@ -114,7 +116,7 @@
 
             final GenericEditorView editor = (GenericEditorView)mInflater.inflate(
                     R.layout.item_generic_editor, mEditors, false);
-            editor.setValues(mKind, entry, mState);
+            editor.setValues(mKind, entry, mState, mReadOnly);
             editor.setEditorListener(this);
             editor.setId(entry.getViewId());
             mEditors.addView(editor);
@@ -129,7 +131,8 @@
     protected void updateAddEnabled() {
         // Set enabled state on the "add" view
         final boolean canInsert = EntityModifier.canInsert(mState, mKind);
-        mAdd.setEnabled(canInsert);
+	final boolean isEnabled = !mReadOnly && canInsert;
+        mAdd.setEnabled(isEnabled);
     }
 
     /** {@inheritDoc} */
diff --git a/src/com/android/contacts/ui/widget/PhotoEditorView.java b/src/com/android/contacts/ui/widget/PhotoEditorView.java
index cde314d..184b907 100644
--- a/src/com/android/contacts/ui/widget/PhotoEditorView.java
+++ b/src/com/android/contacts/ui/widget/PhotoEditorView.java
@@ -74,7 +74,7 @@
     }
 
     /** {@inheritDoc} */
-    public void setValues(DataKind kind, ValuesDelta values, EntityDelta state) {
+    public void setValues(DataKind kind, ValuesDelta values, EntityDelta state, boolean readOnly) {
         mEntry = values;
         if (values != null) {
             // Try decoding photo if actual entry
@@ -85,6 +85,7 @@
 
                 setScaleType(ImageView.ScaleType.CENTER_CROP);
                 setImageBitmap(photo);
+		setEnabled(!readOnly);
                 mHasSetPhoto = true;
             } else {
                 resetDefault();
diff --git a/src/com/android/contacts/util/AccountSelectionUtil.java b/src/com/android/contacts/util/AccountSelectionUtil.java
new file mode 100644
index 0000000..cf83581
--- /dev/null
+++ b/src/com/android/contacts/util/AccountSelectionUtil.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.util;
+
+import com.android.contacts.ImportVCardActivity;
+import com.android.contacts.R;
+import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.GoogleSource;
+import com.android.contacts.model.Sources;
+
+import android.accounts.Account;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+
+import java.util.List;
+
+/**
+ * Utility class for selectiong an Account for importing contact(s)
+ */
+public class AccountSelectionUtil {
+    // TODO: maybe useful for EditContactActivity.java...
+    private static final String LOG_TAG = "AccountSelectionUtil";
+
+    private static class AccountSelectedListener
+            implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
+
+        final private Context mContext;
+        final private List<Account> mAccountList;
+        final private int mResId;
+
+        public AccountSelectedListener(Context context, List<Account> accountList, int resId) {
+            if (accountList == null || accountList.size() == 0) {
+                Log.e(LOG_TAG, "The size of Account list is 0.");
+            }
+            mContext = context;
+            mAccountList = accountList;
+            mResId = resId;
+        }
+
+        public void onClick(DialogInterface dialog, int which) {
+            dialog.dismiss();
+            doImport(mContext, mResId, mAccountList.get(which));
+        }
+
+        public void onCancel(DialogInterface dialog) {
+            dialog.dismiss();
+        }
+    }
+
+    public static Dialog getSelectAccountDialog(Context context, int resId) {
+        return getSelectAccountDialog(context, resId, null);
+    }
+
+    public static Dialog getSelectAccountDialog(Context context, int resId,
+            DialogInterface.OnCancelListener onCancelListener) {
+        final Sources sources = Sources.getInstance(context);
+        final List<Account> writableAccountList = sources.getAccounts(true);
+
+        // Assume accountList.size() > 1
+
+        // Wrap our context to inflate list items using correct theme
+        final Context dialogContext = new ContextThemeWrapper(
+                context, android.R.style.Theme_Light);
+        final LayoutInflater dialogInflater = (LayoutInflater)dialogContext
+                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        final ArrayAdapter<Account> accountAdapter =
+            new ArrayAdapter<Account>(context, android.R.layout.simple_list_item_2,
+                    writableAccountList) {
+
+            @Override
+            public View getView(int position, View convertView, ViewGroup parent) {
+                if (convertView == null) {
+                    convertView = dialogInflater.inflate(
+                            android.R.layout.simple_list_item_2,
+                            parent, false);
+                }
+
+                // TODO: show icon along with title
+                final TextView text1 =
+                        (TextView)convertView.findViewById(android.R.id.text1);
+                final TextView text2 =
+                        (TextView)convertView.findViewById(android.R.id.text2);
+
+                final Account account = this.getItem(position);
+                final ContactsSource source =
+                    sources.getInflatedSource(account.type,
+                            ContactsSource.LEVEL_SUMMARY);
+                final Context context = getContext();
+
+                text1.setText(account.name);
+                text2.setText(source.getDisplayLabel(context));
+
+                return convertView;
+            }
+        };
+
+        AccountSelectedListener accountSelectedListener =
+            new AccountSelectedListener(context, writableAccountList, resId);
+        return new AlertDialog.Builder(context)
+            .setTitle(R.string.dialog_new_contact_account)
+            .setSingleChoiceItems(accountAdapter, 0, accountSelectedListener)
+            .setOnCancelListener(accountSelectedListener)
+            .create();
+    }
+
+    public static void doImport(Context context, int resId, Account account) {
+        switch (resId) {
+            case R.string.import_from_sim: {
+                doImportFromSim(context, account);
+                break;
+            }
+            case R.string.import_from_sdcard: {
+                doImportFromSdCard(context, account);
+                break;
+            }
+        }
+    }
+
+    public static void doImportFromSim(Context context, Account account) {
+        if (account != null) {
+            GoogleSource.createMyContactsIfNotExist(account, context);
+        }
+
+        Intent importIntent = new Intent(Intent.ACTION_VIEW);
+        importIntent.setType("vnd.android.cursor.item/sim-contact");
+        if (account != null) {
+            importIntent.putExtra("account_name", account.name);
+            importIntent.putExtra("account_type", account.type);
+        }
+        importIntent.setClassName("com.android.phone", "com.android.phone.SimContacts");
+        context.startActivity(importIntent);
+    }
+
+    public static void doImportFromSdCard(Context context, Account account) {
+        if (account != null) {
+            GoogleSource.createMyContactsIfNotExist(account, context);
+        }
+
+        Intent importIntent = new Intent(context, ImportVCardActivity.class);
+        if (account != null) {
+            importIntent.putExtra("account_name", account.name);
+            importIntent.putExtra("account_type", account.type);
+        }
+        context.startActivity(importIntent);
+    }
+}