Merge "Import translations. DO NOT MERGE" into ub-contactsdialer-i-dev
diff --git a/AndroidManifest_common.xml b/AndroidManifest_common.xml
index c616c1e..09e9c81 100644
--- a/AndroidManifest_common.xml
+++ b/AndroidManifest_common.xml
@@ -351,9 +351,21 @@
             </intent-filter>
         </activity>
 
-        <activity
+         <!-- Keep support for apps that expect the Compact editor -->
+         <activity-alias android:name="com.android.contacts.activities.CompactContactEditorActivity"
+             android:exported="true"
+             android:targetActivity=".activities.ContactEditorActivity">
+             <intent-filter android:priority="-1">
+                 <action android:name="android.intent.action.INSERT" />
+                 <category android:name="android.intent.category.DEFAULT" />
+                 <data android:mimeType="vnd.android.cursor.dir/person" />
+                 <data android:mimeType="vnd.android.cursor.dir/contact" />
+                 <data android:mimeType="vnd.android.cursor.dir/raw_contact" />
+             </intent-filter>
+         </activity-alias>
+
+         <activity
             android:name=".activities.ContactEditorSpringBoardActivity"
-            android:noHistory="true"
             android:theme="@style/TransparentThemeAppCompat">
 
              <intent-filter>
diff --git a/res/drawable/ic_arrow_drop_down_black_24dp.xml b/res/drawable/ic_arrow_drop_down_black_24dp.xml
new file mode 100644
index 0000000..ebf2123
--- /dev/null
+++ b/res/drawable/ic_arrow_drop_down_black_24dp.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M7,10l5,5 5,-5z"/>
+</vector>
diff --git a/res/drawable/ic_close_24dp.xml b/res/drawable/ic_close_24dp.xml
index 52070d5..bcbb759 100644
--- a/res/drawable/ic_close_24dp.xml
+++ b/res/drawable/ic_close_24dp.xml
@@ -18,7 +18,7 @@
         android:width="24dp"
         android:height="24dp"
         android:viewportWidth="24.0"
-        android:viewportHeight="24.0">
+        android:viewportHeight="24.0" >
     <path
         android:fillColor="@android:color/white"
         android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
diff --git a/res/layout/assistant_new_badge.xml b/res/layout/assistant_new_badge.xml
index 6ca8cc5..21b5297 100644
--- a/res/layout/assistant_new_badge.xml
+++ b/res/layout/assistant_new_badge.xml
@@ -26,6 +26,7 @@
     android:paddingBottom="@dimen/nav_new_badge_vertical_pad"
     android:background="@drawable/new_badge_background"
     android:textSize="@dimen/nav_new_badge_font_size"
+    android:textStyle="bold"
     android:textAllCaps="true"
-    android:textColor="#FFFFFF"
+    android:textColor="@android:color/white"
     android:text="@string/menu_assistant_new_badge"/>
\ No newline at end of file
diff --git a/res/layout/contact_editor_accounts_changed_activity_with_text.xml b/res/layout/contact_editor_accounts_changed_activity_with_text.xml
index 7ee30f7..c9d2039 100644
--- a/res/layout/contact_editor_accounts_changed_activity_with_text.xml
+++ b/res/layout/contact_editor_accounts_changed_activity_with_text.xml
@@ -31,6 +31,7 @@
         android:layout_marginEnd="24dp"
         android:layout_marginTop="24dp"
         android:layout_marginBottom="24dp"
+        android:textAlignment="viewStart"
         android:textAppearance="?android:attr/textAppearanceMedium"/>
 
     <View
@@ -62,7 +63,7 @@
             style="?android:attr/buttonBarButtonStyle"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_toLeftOf="@id/right_button"
+            android:layout_toStartOf="@id/right_button"
             android:layout_marginTop="8dp"/>
 
     </RelativeLayout>
diff --git a/res/layout/contacts_drawer_activity.xml b/res/layout/contacts_drawer_activity.xml
index e560494..0e77058 100644
--- a/res/layout/contacts_drawer_activity.xml
+++ b/res/layout/contacts_drawer_activity.xml
@@ -25,15 +25,6 @@
     android:fitsSystemWindows="true"
     tools:openDrawer="start">
 
-    <!-- To prevent hamburger menu from getting the initial focus. -->
-    <View
-        android:focusable="true"
-        android:focusableInTouchMode="true"
-        android:layout_width="1px"
-        android:layout_height="1px" >
-        <requestFocus/>
-    </View>
-
     <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
         android:id="@+id/fragment_container"
         android:orientation="vertical"
diff --git a/res/layout/fragment_sim_import.xml b/res/layout/fragment_sim_import.xml
index 2da988e..d6d3acf 100644
--- a/res/layout/fragment_sim_import.xml
+++ b/res/layout/fragment_sim_import.xml
@@ -26,14 +26,13 @@
 
         <android.support.v7.widget.Toolbar
             android:id="@+id/toolbar"
-            style="@style/ContactsToolbarStyle"
+            style="@style/LightToolbarStyle"
             android:layout_width="match_parent"
             android:layout_height="?attr/actionBarSize"
-            android:background="?attr/colorPrimary"
-            android:elevation="4dp"
-            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
+            android:elevation="3dp"
+            android:theme="@style/LightToolbarThemeOverlay"
             app:navigationContentDescription="@string/sim_import_cancel_content_description"
-            app:navigationIcon="@drawable/ic_close_dk"
+            app:navigationIcon="@drawable/ic_close_24dp"
             app:title="@string/sim_import_title_none_selected">
 
             <Button
@@ -41,19 +40,56 @@
                 style="@style/Widget.AppCompat.Button.Borderless"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_gravity="right|center_vertical"
+                android:layout_gravity="end|center_vertical"
                 android:text="@string/sim_import_button_text"
                 />
         </android.support.v7.widget.Toolbar>
 
-        <FrameLayout
+
+        <!-- This is managed by AccountHeaderPresenter so the IDs in this section must
+             match the equivalent views in layout/editor_account_header.xml -->
+        <LinearLayout
+            xmlns:android="http://schemas.android.com/apk/res/android"
+            android:id="@+id/account_header_container"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:background="?android:colorBackground"
-            android:elevation="4dp">
+            android:minHeight="48dp"
+            android:orientation="horizontal"
+            android:paddingEnd="32dp"
+            android:paddingStart="16dp">
 
-            <include layout="@layout/editor_account_header"/>
-        </FrameLayout>
+            <ImageView
+                android:id="@+id/account_type_icon"
+                android:layout_width="24dp"
+                android:layout_height="24dp"
+                android:layout_gravity="center_vertical"
+                android:layout_marginEnd="32dp"/>
+
+            <TextView
+                android:id="@+id/account_name"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_vertical"
+                android:layout_weight="1"
+                android:ellipsize="end"
+                android:gravity="center_vertical"
+                android:lines="1"
+                android:maxLines="1"
+                android:textColor="@color/secondary_text_color"
+                android:textSize="16sp"/>
+
+            <ImageView
+                android:id="@+id/account_expander_icon"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginRight="9dp"
+                android:layout_gravity="center_vertical"
+                android:scaleType="center"
+                android:src="@drawable/ic_arrow_drop_down_black_24dp"
+                android:tint="?android:textColorSecondary"
+                android:visibility="gone"/>
+        </LinearLayout>
 
         <FrameLayout
             android:layout_width="match_parent"
@@ -63,8 +99,8 @@
                 android:id="@+id/list"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
-                android:paddingTop="8dp"
-                android:clipToPadding="false"/>
+                android:clipToPadding="false"
+                android:paddingTop="8dp"/>
 
             <android.support.v4.widget.ContentLoadingProgressBar
                 android:id="@+id/loading_progress"
diff --git a/res/layout/item_read_only_field.xml b/res/layout/item_read_only_field.xml
index 3195b4c..e5444a4 100644
--- a/res/layout/item_read_only_field.xml
+++ b/res/layout/item_read_only_field.xml
@@ -31,6 +31,7 @@
         android:layout_width="0dp"
         android:layout_weight="1"
         android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/editor_account_left_margin"
         android:orientation="vertical">
 
         <TextView
diff --git a/res/layout/join_contact_picker_section_header.xml b/res/layout/join_contact_picker_section_header.xml
index 7dafb49..f39cafd 100644
--- a/res/layout/join_contact_picker_section_header.xml
+++ b/res/layout/join_contact_picker_section_header.xml
@@ -23,8 +23,9 @@
     <TextView
         android:id="@+id/text"
         style="@style/ContactListSeparatorTextViewStyle"
-        android:paddingLeft="8dip"
-        android:paddingRight="8dip"
-        android:paddingStart="8dip"
-        android:paddingEnd="8dip" />
+        android:textAlignment="viewStart"
+        android:paddingLeft="24dip"
+        android:paddingRight="24dip"
+        android:paddingStart="24dip"
+        android:paddingEnd="24dip"/>
 </LinearLayout>
diff --git a/res/layout/people_activity.xml b/res/layout/people_activity.xml
index 8810af0..978f47a 100644
--- a/res/layout/people_activity.xml
+++ b/res/layout/people_activity.xml
@@ -20,6 +20,14 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent" >
 
+    <!-- This is kind of a hack. Really we should be able to put the ContactsUnavailableFragment
+         into contacts_list_container but that causes issues with the fragment back stack
+         on older API levels -->
+    <FrameLayout
+        android:id="@+id/contacts_unavailable_container"
+        android:layout_height="match_parent"
+        android:layout_width="match_parent" />
+
     <FrameLayout
         android:id="@+id/contacts_list_container"
         android:layout_height="match_parent"
diff --git a/res/layout/raw_contact_list_item.xml b/res/layout/raw_contact_list_item.xml
index ec5de0c..70bf394 100644
--- a/res/layout/raw_contact_list_item.xml
+++ b/res/layout/raw_contact_list_item.xml
@@ -64,6 +64,8 @@
                 android:id="@+id/account_name"
                 android:textSize="13sp"
                 android:textColor="@color/quantum_black_secondary_text"
+                android:maxLines="1"
+                android:ellipsize="middle"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_gravity="center_vertical"/>
diff --git a/res/menu/activity_main_drawer.xml b/res/menu/activity_main_drawer.xml
index 877c726..8d02e4d 100644
--- a/res/menu/activity_main_drawer.xml
+++ b/res/menu/activity_main_drawer.xml
@@ -29,10 +29,6 @@
             app:actionViewClass="android.widget.LinearLayout"
             android:icon="@drawable/ic_assistant"
             android:title="@string/menu_assistant"/>
-        <item
-            android:id="@+id/nav_find_duplicates"
-            android:icon="@drawable/ic_menu_duplicates"
-            android:title="@string/menu_duplicates"/>
     </group>
 
     <group android:id="@+id/groups">
diff --git a/res/menu/quickcontact.xml b/res/menu/quickcontact.xml
index 4d00a67..7d87708 100644
--- a/res/menu/quickcontact.xml
+++ b/res/menu/quickcontact.xml
@@ -26,14 +26,14 @@
         android:showAsAction="always" />
 
     <item
-        android:id="@+id/menu_split"
-        android:title="@string/menu_splitAggregate" />
-
-    <item
         android:id="@+id/menu_join"
         android:title="@string/menu_joinAggregate" />
 
     <item
+        android:id="@+id/menu_linked_contacts"
+        android:title="@string/menu_linkedContacts" />
+
+    <item
         android:id="@+id/menu_delete"
         android:title="@string/menu_deleteContact" />
 
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 03ca621..27759b9 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -42,7 +42,7 @@
     <!-- Color of the status bar above the contextual selection bar. -->
     <color name="contextual_selection_bar_status_bar_color">#bababa</color>
 
-    <color name="primary_color_dark">#2a56c6</color>
+    <color name="primary_color_dark">#1c3aa9</color>
     <color name="primary_color">#2a56c6</color>
 
     <color name="group_primary_color_dark">#546E7A</color>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index d3431fb..d603973 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -148,8 +148,8 @@
     <!-- For join screen. Mainly for tablet. -->
     <dimen name="join_header_left_margin">@dimen/contact_browser_list_header_left_margin</dimen>
     <dimen name="join_header_right_margin">@dimen/contact_browser_list_header_right_margin</dimen>
-    <dimen name="join_header_top_margin">16dip</dimen>
-    <dimen name="join_header_bottom_margin">0dip</dimen>
+    <dimen name="join_header_top_margin">12dip</dimen>
+    <dimen name="join_header_bottom_margin">12dip</dimen>
 
     <dimen name="contact_filter_header_min_height">24dip</dimen>
 
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 29c92c2..9a1dd19 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -147,9 +147,20 @@
     <!-- Positive button text from the confirmation dialog for joining contacts when there are unsaved changes. [CHAR LIMIT = 60] -->
     <string name="joinConfirmation_positive_button">Save and Link</string>
 
+    <!-- The text to show on on a ProgressDialog indicating we're currently linking
+         contacts [CHAR LIMIT=20]-->
+    <string name="contacts_linking_progress_bar">Linking</string>
+
+    <!-- The text to show on on a ProgressDialog indicating we're currently unlinking
+     contacts [CHAR LIMIT=20]-->
+    <string name="contacts_unlinking_progress_bar">Unlinking</string>
+
     <!-- Menu item that links an aggregate with another aggregate -->
     <string name="menu_joinAggregate">Link</string>
 
+    <!-- Menu item that opens a dialog that shows all linked contacts that make up this one. [CHAR LIMIT=30] -->
+    <string name="menu_linkedContacts">View linked contacts</string>
+
     <!-- Menu item (in the action bar) to indicate that changes should be saved [CHAR LIMIT=20] -->
     <string name="menu_save">Save</string>
 
@@ -218,13 +229,13 @@
     <string name="menu_unredirect_calls_to_vm">Unroute to voicemail</string>
 
     <!-- Warning dialog contents after users select to delete a ReadOnly contact. [CHAR LIMIT=NONE] -->
-    <string name="readOnlyContactWarning">Contacts from your read-only accounts cannot be deleted, but they can be hidden.</string>
+    <string name="readOnlyContactWarning">This contact is read-only. It can\'t be deleted, but you can hide it.</string>
 
     <!-- Positive button text of the warning dialog contents after users select to delete a ReadOnly contact. [CHAR LIMIT=30]-->
-    <string name="readOnlyContactWarning_positive_button">Hide</string>
+    <string name="readOnlyContactWarning_positive_button">Hide Contact</string>
 
     <!-- Warning dialog contents after users selects to delete a contact with ReadOnly and Writable sources. [CHAR LIMIT=NONE]-->
-    <string name="readOnlyContactDeleteConfirmation">The contact to be deleted has details from multiple accounts. Details from read-only accounts will be hidden, not deleted.</string>
+    <string name="readOnlyContactDeleteConfirmation">Read-only accounts in this contact will be hidden, not deleted.</string>
 
     <!-- Confirmation dialog. Shown after user selects to delete one writable contact [CHAR LIMIT=NONE]  -->
     <string name="single_delete_confirmation">Delete this contact?</string>
@@ -374,8 +385,8 @@
     <!-- Option name to send message to all members of a group/selection [CHAR LIMIT=30] -->
     <string name="menu_sendMessageOption">Send message</string>
 
-    <!-- Activity title when the user is selecting items [CHAR LIMIT=128] -->
-    <string name="pickerSelectContactsActivityTitle">Select Contacts</string>
+    <!-- Activity title when the user is selecting items [CHAR LIMIT=40] -->
+    <string name="pickerSelectContactsActivityTitle">Choose Contacts</string>
 
     <!-- The menu item to send the currently selected contacts to selected items [CHAR LIMIT=10] -->
     <string name="send_to_selection">Send</string>
@@ -720,8 +731,22 @@
          at a pre-determined text size. [CHAR LIMIT=20] -->
     <string name="contact_editor_title_existing_contact">Edit contact</string>
 
+    <!-- Title of the editor activity when viewing a read-only raw contact. The char
+         limit is short and cannot be increased, since this needs to be displayed in a single line
+         at a pre-determined text size. [CHAR LIMIT=20] -->
+    <string name="contact_editor_title_read_only_contact">View only</string>
+
     <!-- Dialog title when the user is selecting a raw contact to edit.  [CHAR LIMIT=128] -->
-    <string name="contact_editor_pick_raw_contact_dialog_title">Choose linked contact</string>
+    <string name="contact_editor_pick_raw_contact_to_edit_dialog_title">Choose contact to edit</string>
+
+    <!-- Dialog title showing the user the list of contacts that are linked together to create the aggregate they are viewing.  [CHAR LIMIT=128] -->
+    <string name="contact_editor_pick_linked_contact_dialog_title">Linked contacts</string>
+
+    <!-- Dialog button label to prompt user to add more linked contacts to the one they're viewing. [CHAR LIMIT=15] -->
+    <string name="contact_editor_add_linked_contact">Add</string>
+
+    <!-- Dialog button label to prompt user to unlinked the contact they're looking at to it's constituent parts. [CHAR LIMIT=15] -->
+    <string name="contact_editor_unlink_contacts">Unlink</string>
 
     <!-- Button label to prompt the user to add an account (when there are 0 existing accounts on the device) [CHAR LIMIT=30] -->
     <string name="add_account">Add account</string>
@@ -729,7 +754,7 @@
     <!-- Button label to prompt the user to add another account (when there are already existing accounts on the device) [CHAR LIMIT=30] -->
     <string name="add_new_account">Add new account</string>
 
-    <!-- Menu item shown only when the special debug mode is enabled, which is used to send all contacts database files via email.  [CHAR LIMI=NONE] -->
+    <!-- Menu item shown only when the special debug mode is enabled, which is used to send all contacts database files via email.  [CHAR LIMIT=NONE] -->
     <string name="menu_export_database">Export database files</string>
 
     <!-- Content description for the button that adds a new contact
@@ -832,8 +857,8 @@
     <!-- Label for the account selector to indicate which account a contact will be saved to. [CHAR LIMIT=30] -->
     <string name="editor_account_selector_title">Saving to</string>
 
-    <!-- Label for the account selector to indicate which read-only account is being viewed. [CHAR LIMIT=30] -->
-    <string name="editor_account_selector_read_only_title">Viewing</string>
+    <!-- Label for the account selector to indicate the current information comes from a read-only account. [CHAR LIMIT=50] -->
+    <string name="editor_account_selector_read_only_title">Contact info from <xliff:g id="account">%s</xliff:g> is not editable</string>
 
     <!-- Content description for the account selector to indicate which account a contact will be saved to. [CHAR LIMIT=NONE] -->
     <string name="editor_account_selector_description">Currently saving to <xliff:g id="account_name">%s</xliff:g>. Double-tap to pick a different account.</string>
@@ -920,13 +945,10 @@
     <!-- Content description of photo in photo picker indicating a photo from unknown account is selected. -->
     <string name="photo_view_description_checked_no_info">Photo from unknown account checked</string>
 
-    <!-- Left drawer menu item to open contacts assistant cards. [CHAR LIMIT=20]-->
-    <string name="menu_assistant">Assistant</string>
+    <!-- Left drawer menu item to open contacts assistant/suggestion cards. [CHAR LIMIT=20]-->
+    <string name="menu_assistant">Suggestions</string>
 
-    <!-- The menu item to open the link/merge duplicates activity. [CHAR LIMIT=20]-->
-    <string name="menu_duplicates">Duplicates</string>
-
-    <!-- Assistant 'NEW' badge text. Badge shown next to Assistant in left drawer menu when user has not visited it yet. [CHAR LIMIT=10] -->
+    <!-- Badge shown next to Suggestions in left drawer menu when user has not visited it yet. [CHAR LIMIT=10] -->
     <string name="menu_assistant_new_badge">New</string>
 
     <!-- Open drawer content descriptions [CHAR LIMIT=40] -->
@@ -947,7 +969,7 @@
 
     <!-- Content displayed in QuickContact activity after Contacts app receiving
          Calendar and SMS permissions [CHAR LIMIT=60] -->
-    <string name="permission_explanation_subheader_calendar_and_SMS">Events and Messages</string>
+    <string name="permission_explanation_subheader_calendar_and_SMS">Events and messages</string>
 
     <!-- Content displayed in QuickContact activity after Contacts app receiving
          Calendar permission [CHAR LIMIT=40] -->
@@ -958,9 +980,9 @@
     <string name="permission_explanation_subheader_SMS">Messages</string>
 
     <!-- The header text for hamburger promo [CHAR LIMIT=60]-->
-    <string name="hamburger_feature_highlight_header">Organize your list</string>
+    <string name="hamburger_feature_highlight_header">Suggestions</string>
     <!-- The body text for hamburger promo [CHAR LIMIT=200]-->
-    <string name="hamburger_feature_highlight_body">Clean up duplicates &amp; group contacts by label</string>
+    <string name="hamburger_feature_highlight_body">Keep your contacts organized and useful</string>
 
     <!-- The label for the action shown in a snackbar after an operation that modifies some data is performed.
          The user can click on the action to rollback the modification-->
@@ -1836,9 +1858,6 @@
     <!-- Toolbar title shown when importing SIM contacts and none are selected -->
     <string name="sim_import_title_none_selected">Select contacts</string>
 
-    <!-- Toolbar title shown when importing SIM contacts and some are selected -->
-    <string name="sim_import_title_some_selected_fmt"><xliff:g id="count">%d</xliff:g> Selected</string>
-
     <!-- Message shown when the SIM import screen is displayed but there are no contacts on the
          SIM card  -->
     <string name="sim_import_empty_message">No contacts on your SIM card</string>
@@ -1875,11 +1894,10 @@
     <string name="account_sync_off">Account sync is off. Tap to turn on.</string>
 
     <!-- Title of dialog to turn auto-sync on [CHAR LIMIT=100] -->
-    <string name="turn_auto_sync_on_dialog_title">Turn auto-sync on?</string>
+    <string name="turn_auto_sync_on_dialog_title">Turn on auto-sync?</string>
 
     <!-- Text of dialog to turn auto-sync on [CHAR LIMIT=500] -->
-    <string name="turn_auto_sync_on_dialog_body">Changes you make to all apps and accounts,
-        not just Contacts, will be synchronized between the web and your devices.</string>
+    <string name="turn_auto_sync_on_dialog_body">Changes you make to all apps and accounts, not just Google Contacts, will be kept up to date between the web and your devices.</string>
 
     <!-- Confirm button text for dialog to turn auto-sync on [CHAR LIMIT=30] -->
     <string name="turn_auto_sync_on_dialog_confirm_btn">Turn on</string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index ad0915e..b762177 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -91,6 +91,8 @@
         <item name="list_item_avatar_offset_top">-1dp</item>
         <!-- Favorites -->
         <item name="favorites_padding_bottom">0dip</item>
+        <!-- Popup menu -->
+        <item name="android:popupMenuStyle">@style/PopupMenuStyle</item>
     </style>
 
     <style name="PeopleActivityTheme" parent="@style/PeopleThemeAppCompat">
@@ -166,6 +168,17 @@
         <!-- Favorites -->
         <item name="favorites_padding_bottom">0dip</item>
         <item name="drawerArrowStyle">@style/DrawerArrowStyle</item>
+        <!-- Popup menu -->
+        <item name="android:popupMenuStyle">@style/PopupMenuStyleAppCompat</item>
+        <item name="popupMenuStyle">@style/PopupMenuStyleAppCompat</item>
+    </style>
+
+    <style name="PopupMenuStyle" parent="@android:style/Widget.PopupMenu">
+        <item name="android:popupBackground">@android:color/white</item>
+    </style>
+
+    <style name="PopupMenuStyleAppCompat" parent="Widget.AppCompat.PopupMenu">
+        <item name="android:popupBackground">@android:color/white</item>
     </style>
 
     <style name="ContactsActionButtonStyle" parent="Widget.AppCompat.ActionButton">
@@ -218,6 +231,21 @@
         <item name="titleTextAppearance">@style/ContactsActionBarTitleTextAppCompat</item>
     </style>
 
+    <style name="LightToolbarNavigationButtonStyle" parent="@style/Widget.AppCompat.Toolbar.Button.Navigation">
+        <item name="android:tint">?android:textColorSecondary</item>
+    </style>
+
+    <style name="LightToolbarThemeOverlay" parent="@style/ThemeOverlay.AppCompat.ActionBar">
+        <item name="toolbarNavigationButtonStyle">@style/LightToolbarNavigationButtonStyle</item>
+    </style>
+
+    <style name="LightToolbarStyle" parent="@style/Widget.AppCompat.Toolbar">
+        <item name="android:background">@color/contextual_selection_bar_color</item>
+        <item name="background">@color/contextual_selection_bar_color</item>
+        <item name="android:titleTextAppearance">@style/ContactsActionBarTitleTextBlack</item>
+        <item name="titleTextAppearance">@style/ContactsActionBarTitleTextBlack</item>
+    </style>
+
     <!-- Text in the action bar at the top of the screen -->
     <style name="ContactsActionBarTitleText"
            parent="@android:style/TextAppearance.Material.Widget.ActionBar.Title">
diff --git a/src-bind/com/android/contactsbind/ObjectFactory.java b/src-bind/com/android/contactsbind/ObjectFactory.java
index d6799e2..af11c90 100644
--- a/src-bind/com/android/contactsbind/ObjectFactory.java
+++ b/src-bind/com/android/contactsbind/ObjectFactory.java
@@ -39,11 +39,7 @@
         return new DeviceLocalAccountTypeFactory.Default(context);
     }
 
-    public static Fragment getAssistantFragment(String tag) {
-        return null;
-    }
-
-    public static Fragment getDuplicatesUtilFragment() {
+    public static Fragment getAssistantFragment() {
         return null;
     }
 
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index 853e676..24bcbfd 100755
--- a/src/com/android/contacts/ContactSaveService.java
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -16,6 +16,8 @@
 
 package com.android.contacts;
 
+import static android.Manifest.permission.WRITE_CONTACTS;
+
 import android.app.Activity;
 import android.app.IntentService;
 import android.content.ContentProviderOperation;
@@ -45,10 +47,9 @@
 import android.provider.ContactsContract.Profile;
 import android.provider.ContactsContract.RawContacts;
 import android.provider.ContactsContract.RawContactsEntity;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
 import android.support.v4.content.LocalBroadcastManager;
 import android.support.v4.os.ResultReceiver;
+import android.telephony.SubscriptionInfo;
 import android.text.TextUtils;
 import android.util.Log;
 import android.widget.Toast;
@@ -62,6 +63,7 @@
 import com.android.contacts.common.model.RawContactDelta;
 import com.android.contacts.common.model.RawContactDeltaList;
 import com.android.contacts.common.model.RawContactModifier;
+import com.android.contacts.common.model.SimCard;
 import com.android.contacts.common.model.SimContact;
 import com.android.contacts.common.model.account.AccountWithDataSet;
 import com.android.contacts.common.preference.ContactsPreferences;
@@ -75,12 +77,11 @@
 import com.google.common.collect.Sets;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
 
-import static android.Manifest.permission.WRITE_CONTACTS;
-
 /**
  * A service responsible for saving changes to the content provider.
  */
@@ -131,6 +132,7 @@
     public static final String EXTRA_DATA_ID = "dataId";
 
     public static final String ACTION_SPLIT_CONTACT = "splitContact";
+    public static final String EXTRA_HARD_SPLIT = "extraHardSplit";
 
     public static final String ACTION_JOIN_CONTACTS = "joinContacts";
     public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
@@ -149,10 +151,18 @@
 
     public static final String ACTION_IMPORT_FROM_SIM = "importFromSim";
     public static final String EXTRA_SIM_CONTACTS = "simContacts";
+    public static final String EXTRA_SIM_SUBSCRIPTION_ID = "simSubscriptionId";
+
+    // For debugging and testing what happens when requests are queued up.
+    public static final String ACTION_SLEEP = "sleep";
+    public static final String EXTRA_SLEEP_DURATION = "sleepDuration";
 
     public static final String BROADCAST_GROUP_DELETED = "groupDeleted";
     public static final String BROADCAST_SIM_IMPORT_COMPLETE = "simImportComplete";
-    public static final String EXTRA_CALLBACK_DATA = "extraCallbackData";
+    public static final String BROADCAST_LINK_COMPLETE = "linkComplete";
+    public static final String BROADCAST_UNLINK_COMPLETE = "unlinkComplete";
+
+    public static final String BROADCAST_SERVICE_STATE_CHANGED = "serviceStateChanged";
 
     public static final String EXTRA_RESULT_CODE = "resultCode";
     public static final String EXTRA_RESULT_COUNT = "count";
@@ -197,6 +207,9 @@
     private static final CopyOnWriteArrayList<Listener> sListeners =
             new CopyOnWriteArrayList<Listener>();
 
+    // Holds the current state of the service
+    private static final State sState = new State();
+
     private Handler mMainHandler;
     private GroupsDao mGroupsDao;
     private SimContactDao mSimContactDao;
@@ -230,6 +243,15 @@
         sListeners.remove(listener);
     }
 
+    public static State getState() {
+        return sState;
+    }
+
+    private void notifyStateChanged() {
+        LocalBroadcastManager.getInstance(this)
+                .sendBroadcast(new Intent(BROADCAST_SERVICE_STATE_CHANGED));
+    }
+
     /**
      * Returns true if the ContactSaveService was started successfully and false if an exception
      * was thrown and a Toast error message was displayed.
@@ -279,8 +301,17 @@
         return getApplicationContext().getSystemService(name);
     }
 
+    // Parent classes Javadoc says not to override this method but we're doing it just to update
+    // our state which should be OK since we're still doing the work in onHandleIntent
     @Override
-    protected void onHandleIntent(Intent intent) {
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        sState.onStart(intent);
+        notifyStateChanged();
+        return super.onStartCommand(intent, flags, startId);
+    }
+
+    @Override
+    protected void onHandleIntent(final Intent intent) {
         if (intent == null) {
             Log.d(TAG, "onHandleIntent: could not handle null intent");
             return;
@@ -332,7 +363,12 @@
             undo(intent);
         } else if (ACTION_IMPORT_FROM_SIM.equals(action)) {
             importFromSim(intent);
+        } else if (ACTION_SLEEP.equals(action)) {
+            sleepForDebugging(intent);
         }
+
+        sState.onFinish(intent);
+        notifyStateChanged();
     }
 
     /**
@@ -1210,7 +1246,7 @@
 
     /**
      * Creates an intent that can be sent to this service to split a contact into it's constituent
-     * pieces. This will set the raw contact ids to TYPE_AUTOMATIC for AggregationExceptions so
+     * pieces. This will set the raw contact ids to {@link AggregationExceptions#TYPE_AUTOMATIC} so
      * they may be re-merged by the auto-aggregator.
      */
     public static Intent createSplitContactIntent(Context context, long[][] rawContactIds,
@@ -1222,10 +1258,24 @@
         return serviceIntent;
     }
 
+    /**
+     * Creates an intent that can be sent to this service to split a contact into it's constituent
+     * pieces. This will explicitly set the raw contact ids to
+     * {@link AggregationExceptions#TYPE_KEEP_SEPARATE}.
+     */
+    public static Intent createHardSplitContactIntent(Context context, long[][] rawContactIds) {
+        final Intent serviceIntent = new Intent(context, ContactSaveService.class);
+        serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
+        serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
+        serviceIntent.putExtra(ContactSaveService.EXTRA_HARD_SPLIT, true);
+        return serviceIntent;
+    }
+
     private void splitContact(Intent intent) {
         final long rawContactIds[][] = (long[][]) intent
                 .getSerializableExtra(EXTRA_RAW_CONTACT_IDS);
         final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
+        final boolean hardSplit = intent.getBooleanExtra(EXTRA_HARD_SPLIT, false);
         if (rawContactIds == null) {
             Log.e(TAG, "Invalid argument for splitContact request");
             if (receiver != null) {
@@ -1239,7 +1289,8 @@
         for (int i = 0; i < rawContactIds.length; i++) {
             for (int j = 0; j < rawContactIds.length; j++) {
                 if (i != j) {
-                    if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j])) {
+                    if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j],
+                            hardSplit)) {
                         if (receiver != null) {
                             receiver.send(CP2_ERROR, new Bundle());
                             return;
@@ -1254,6 +1305,8 @@
             }
             return;
         }
+        LocalBroadcastManager.getInstance(this)
+                .sendBroadcast(new Intent(BROADCAST_UNLINK_COMPLETE));
         if (receiver != null) {
             receiver.send(CONTACTS_SPLIT, new Bundle());
         } else {
@@ -1267,7 +1320,7 @@
      * @return false if an error occurred, true otherwise.
      */
     private boolean buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations,
-            long[] rawContactIds1, long[] rawContactIds2) {
+            long[] rawContactIds1, long[] rawContactIds2, boolean hardSplit) {
         if (rawContactIds1 == null || rawContactIds2 == null) {
             Log.e(TAG, "Invalid arguments for splitContact request");
             return false;
@@ -1278,7 +1331,7 @@
         final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
         for (int i = 0; i < rawContactIds1.length; i++) {
             for (int j = 0; j < rawContactIds2.length; j++) {
-                buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j]);
+                buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j], hardSplit);
                 // Before we get to 500 we need to flush the operations list
                 if (operations.size() > 0 && operations.size() % batchSize == 0) {
                     if (!applyOperations(resolver, operations)) {
@@ -1419,6 +1472,8 @@
                     showToast(R.string.contactsJoinedNamedMessage, name);
                 }
             }
+            LocalBroadcastManager.getInstance(this)
+                    .sendBroadcast(new Intent(BROADCAST_LINK_COMPLETE));
         } else {
             if (receiver != null) {
                 receiver.send(CP2_ERROR, new Bundle());
@@ -1557,6 +1612,8 @@
             Uri uri = RawContacts.getContactLookupUri(resolver,
                     ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
             callbackIntent.setData(uri);
+            LocalBroadcastManager.getInstance(this)
+                    .sendBroadcast(new Intent(BROADCAST_LINK_COMPLETE));
         }
         deliverCallback(callbackIntent);
     }
@@ -1676,48 +1733,99 @@
     }
 
     /**
-     * Construct a {@link AggregationExceptions#TYPE_AUTOMATIC} ContentProviderOperation.
+     * Construct a {@link AggregationExceptions#TYPE_AUTOMATIC} or a
+     * {@link AggregationExceptions#TYPE_KEEP_SEPARATE} ContentProviderOperation if a hard split is
+     * requested.
      */
     private void buildSplitContactDiff(ArrayList<ContentProviderOperation> operations,
-            long rawContactId1, long rawContactId2) {
+            long rawContactId1, long rawContactId2, boolean hardSplit) {
         final Builder builder =
                 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
-        builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_AUTOMATIC);
+        builder.withValue(AggregationExceptions.TYPE,
+                hardSplit
+                        ? AggregationExceptions.TYPE_KEEP_SEPARATE
+                        : AggregationExceptions.TYPE_AUTOMATIC);
         builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
         builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
         operations.add(builder.build());
     }
 
-    public static Intent createImportFromSimIntent(@NonNull Context context,
-            @NonNull ArrayList<SimContact> contacts, @NonNull AccountWithDataSet targetAccount,
-            @Nullable  Bundle callbackData) {
+    /**
+     * Returns an intent that can be used to import the contacts into targetAccount.
+     *
+     * @param context context to use for creating the intent
+     * @param subscriptionId the subscriptionId of the SIM card that is being imported. See
+     *                       {@link SubscriptionInfo#getSubscriptionId()}. Upon completion the
+     *                       SIM for that subscription ID will be marked as imported
+     * @param contacts the contacts to import
+     * @param targetAccount the account import the contacts into
+     */
+    public static Intent createImportFromSimIntent(Context context, int subscriptionId,
+            ArrayList<SimContact> contacts, AccountWithDataSet targetAccount) {
         return new Intent(context, ContactSaveService.class)
                 .setAction(ACTION_IMPORT_FROM_SIM)
                 .putExtra(EXTRA_SIM_CONTACTS, contacts)
-                .putExtra(EXTRA_ACCOUNT, targetAccount)
-                .putExtra(EXTRA_CALLBACK_DATA, callbackData);
+                .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId)
+                .putExtra(EXTRA_ACCOUNT, targetAccount);
     }
 
     private void importFromSim(Intent intent) {
         final Intent result = new Intent(BROADCAST_SIM_IMPORT_COMPLETE)
                 .putExtra(EXTRA_OPERATION_REQUESTED_AT_TIME, System.currentTimeMillis());
+        final int subscriptionId = intent.getIntExtra(EXTRA_SIM_SUBSCRIPTION_ID,
+                SimCard.NO_SUBSCRIPTION_ID);
         try {
             final AccountWithDataSet targetAccount = intent.getParcelableExtra(EXTRA_ACCOUNT);
             final ArrayList<SimContact> contacts =
                     intent.getParcelableArrayListExtra(EXTRA_SIM_CONTACTS);
             mSimContactDao.importContacts(contacts, targetAccount);
+
+            // Update the imported state of the SIM card that was imported
+            final SimCard sim = mSimContactDao.getSimBySubscriptionId(subscriptionId);
+            if (sim != null) {
+                mSimContactDao.persistSimState(sim.withImportedState(true));
+            }
+
             // notify success
             LocalBroadcastManager.getInstance(this).sendBroadcast(result
                     .putExtra(EXTRA_RESULT_COUNT, contacts.size())
                     .putExtra(EXTRA_RESULT_CODE, RESULT_SUCCESS)
-                    .putExtra(EXTRA_CALLBACK_DATA, intent.getBundleExtra(EXTRA_CALLBACK_DATA)));
+                    .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId));
             if (Log.isLoggable(TAG, Log.DEBUG)) {
                 Log.d(TAG, "importFromSim completed successfully");
             }
         } catch (RemoteException|OperationApplicationException e) {
             FeedbackHelper.sendFeedback(this, TAG, "Failed to import contacts from SIM card", e);
             LocalBroadcastManager.getInstance(this).sendBroadcast(result
-                    .putExtra(EXTRA_RESULT_CODE, RESULT_FAILURE));
+                    .putExtra(EXTRA_RESULT_CODE, RESULT_FAILURE)
+                    .putExtra(EXTRA_SIM_SUBSCRIPTION_ID, subscriptionId));
+        }
+    }
+
+    /**
+     * Returns an intent that can start this service and cause it to sleep for the specified time.
+     *
+     * This exists purely for debugging and manual testing. Since this service uses a single thread
+     * it is useful to have a way to test behavior when work is queued up and most of the other
+     * operations complete too quickly to simulate that under normal conditions.
+     */
+    public static Intent createSleepIntent(Context context, long millis) {
+        return new Intent(context, ContactSaveService.class).setAction(ACTION_SLEEP)
+                .putExtra(EXTRA_SLEEP_DURATION, millis);
+    }
+
+    private void sleepForDebugging(Intent intent) {
+        long duration = intent.getLongExtra(EXTRA_SLEEP_DURATION, 1000);
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "sleeping for " + duration + "ms");
+        }
+        try {
+            Thread.sleep(duration);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "finished sleeping");
         }
     }
 
@@ -1874,4 +1982,62 @@
             return contentResolver.delete(groupUri, null, null);
         }
     }
+
+    /**
+     * Keeps track of which operations have been requested but have not yet finished for this
+     * service.
+     */
+    public static class State {
+        private final CopyOnWriteArrayList<Intent> mPending;
+
+        public State() {
+            mPending = new CopyOnWriteArrayList<>();
+        }
+
+        public State(Collection<Intent> pendingActions) {
+            mPending = new CopyOnWriteArrayList<>(pendingActions);
+        }
+
+        public boolean isIdle() {
+            return mPending.isEmpty();
+        }
+
+        public Intent getCurrentIntent() {
+            return mPending.isEmpty() ? null : mPending.get(0);
+        }
+
+        /**
+         * Returns the first intent requested that has the specified action or null if no intent
+         * with that action has been requested.
+         */
+        public Intent getNextIntentWithAction(String action) {
+            for (Intent intent : mPending) {
+                if (action.equals(intent.getAction())) {
+                    return intent;
+                }
+            }
+            return null;
+        }
+
+        public boolean isActionPending(String action) {
+            return getNextIntentWithAction(action) != null;
+        }
+
+        private void onFinish(Intent intent) {
+            if (mPending.isEmpty()) {
+                return;
+            }
+            final String action = mPending.get(0).getAction();
+            if (action.equals(intent.getAction())) {
+                mPending.remove(0);
+            }
+        }
+
+        private void onStart(Intent intent) {
+            if (intent.getAction() == null) {
+                return;
+            }
+            mPending.add(intent);
+        }
+    }
 }
diff --git a/src/com/android/contacts/ContactsDrawerActivity.java b/src/com/android/contacts/ContactsDrawerActivity.java
index 29ac4f8..79d0fe2 100644
--- a/src/com/android/contacts/ContactsDrawerActivity.java
+++ b/src/com/android/contacts/ContactsDrawerActivity.java
@@ -47,7 +47,6 @@
 
 import com.android.contacts.activities.ActionBarAdapter;
 import com.android.contacts.common.ContactsUtils;
-import com.android.contacts.common.Experiments;
 import com.android.contacts.common.compat.CompatUtils;
 import com.android.contacts.common.list.AccountFilterActivity;
 import com.android.contacts.common.list.ContactListFilter;
@@ -78,7 +77,6 @@
 import com.android.contacts.util.SharedPreferenceUtil;
 import com.android.contactsbind.HelpUtils;
 import com.android.contactsbind.ObjectFactory;
-import com.android.contactsbind.experiments.Flags;
 
 import java.util.HashMap;
 import java.util.Iterator;
@@ -141,7 +139,6 @@
             // overlaid by the action bar of the newly-created fragment.
             stopSearchAndSelection();
             updateStatusBarBackground();
-            initializeAssistantNewBadge();
         }
 
         private void stopSearchAndSelection() {
@@ -184,6 +181,7 @@
                 mRunnable.run();
                 mRunnable = null;
             }
+            initializeAssistantNewBadge();
         }
 
         public void runWhenIdle(Runnable runnable) {
@@ -264,11 +262,15 @@
     }
 
     private void initializeAssistantNewBadge() {
-        if (!Flags.getInstance().getBoolean(Experiments.ASSISTANT)) {
+        if (mNavigationView == null) {
             return;
         }
-        final LinearLayout newBadgeFrame = (LinearLayout) MenuItemCompat.getActionView(
-                mNavigationView.getMenu().findItem(R.id.nav_assistant));
+        final MenuItem assistantMenu = mNavigationView.getMenu().findItem(R.id.nav_assistant);
+        if (assistantMenu == null) {
+            return;
+        }
+        final LinearLayout newBadgeFrame =
+                (LinearLayout) MenuItemCompat.getActionView(assistantMenu);
         final boolean showWelcomeBadge = !SharedPreferenceUtil.isWelcomeCardDismissed(this);
         if (showWelcomeBadge && newBadgeFrame.getChildCount() == 0) {
             if (mAssistantNewBadge == null) {
@@ -301,19 +303,10 @@
     private void setUpMenu() {
         final Menu menu = mNavigationView.getMenu();
 
-        if (ObjectFactory.getDuplicatesUtilFragment() == null) {
+        if (ObjectFactory.getAssistantFragment() == null) {
             menu.removeItem(R.id.nav_assistant);
-            menu.removeItem(R.id.nav_find_duplicates);
         } else {
-            int id;
-            if (Flags.getInstance().getBoolean(Experiments.ASSISTANT)) {
-                id = R.id.nav_assistant;
-                menu.removeItem(R.id.nav_find_duplicates);
-            } else {
-                id = R.id.nav_find_duplicates;
-                menu.removeItem(R.id.nav_assistant);
-            }
-
+            final int id = R.id.nav_assistant;
             final MenuItem assistantMenu = menu.findItem(id);
             mIdMenuMap.put(id, assistantMenu);
             if (isAssistantView()) {
@@ -630,7 +623,7 @@
                     HelpUtils.launchHelpAndFeedbackForMainScreen(ContactsDrawerActivity.this);
                 } else if (id == R.id.nav_all_contacts) {
                     switchToAllContacts();
-                } else if (id == R.id.nav_assistant || id == R.id.nav_find_duplicates) {
+                } else if (id == R.id.nav_assistant) {
                     if (!isAssistantView()) {
                         launchAssistant();
                         updateMenuSelection(item);
@@ -714,9 +707,9 @@
             onAccountChosen(accounts.get(0), /* extraArgs */ null);
             return;
         }
-        SelectAccountDialogFragment.show(getFragmentManager(), null,
-                R.string.dialog_new_group_account, AccountListFilter.ACCOUNTS_GROUP_WRITABLE,
-                /* extraArgs */ null, TAG_SELECT_ACCOUNT_DIALOG);
+        SelectAccountDialogFragment.show(getFragmentManager(), R.string.dialog_new_group_account,
+                AccountListFilter.ACCOUNTS_GROUP_WRITABLE, /* extraArgs */ null,
+                TAG_SELECT_ACCOUNT_DIALOG);
     }
 
     @Override
diff --git a/src/com/android/contacts/DynamicShortcuts.java b/src/com/android/contacts/DynamicShortcuts.java
index fce2f8b..52307e4 100644
--- a/src/com/android/contacts/DynamicShortcuts.java
+++ b/src/com/android/contacts/DynamicShortcuts.java
@@ -407,8 +407,6 @@
             final Flags flags = Flags.getInstance();
             Log.d(TAG, "DyanmicShortcuts.initialize\nVERSION >= N_MR1? " +
                     (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) +
-                    "\n" + Experiments.DYNAMIC_SHORTCUTS + " enabled? " +
-                    flags.getBoolean(Experiments.DYNAMIC_SHORTCUTS) +
                     "\nisJobScheduled? " + isJobScheduled(context) +
                     "\nminDelay=" +
                     flags.getInteger(Experiments.DYNAMIC_MIN_CONTENT_CHANGE_UPDATE_DELAY_MILLIS) +
@@ -420,11 +418,7 @@
 
         final DynamicShortcuts shortcuts = new DynamicShortcuts(context);
 
-        if (!Flags.getInstance().getBoolean(Experiments.DYNAMIC_SHORTCUTS)) {
-            // Clear dynamic shortcuts if the flag is not enabled. This prevents shortcuts from
-            // staying around if it is enabled then later disabled (due to bugs for instance).
-            shortcuts.handleFlagDisabled();
-        } else if (!shortcuts.hasRequiredPermissions()) {
+        if (!shortcuts.hasRequiredPermissions()) {
             final IntentFilter filter = new IntentFilter();
             filter.addAction(RequestPermissionsActivity.BROADCAST_PERMISSIONS_GRANTED);
             LocalBroadcastManager.getInstance(shortcuts.mContext).registerReceiver(
diff --git a/src/com/android/contacts/SimImportFragment.java b/src/com/android/contacts/SimImportFragment.java
index c43caaa..66be41b 100644
--- a/src/com/android/contacts/SimImportFragment.java
+++ b/src/com/android/contacts/SimImportFragment.java
@@ -27,11 +27,13 @@
 import android.support.annotation.Nullable;
 import android.support.design.widget.Snackbar;
 import android.support.v4.util.ArrayMap;
+import android.support.v4.view.ViewCompat;
 import android.support.v4.widget.ContentLoadingProgressBar;
 import android.support.v7.widget.Toolbar;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.AbsListView;
 import android.widget.AdapterView;
 import android.widget.ListView;
 
@@ -47,9 +49,11 @@
 import com.android.contacts.common.model.account.AccountWithDataSet;
 import com.android.contacts.common.preference.ContactsPreferences;
 import com.android.contacts.editor.AccountHeaderPresenter;
+import com.google.common.primitives.Longs;
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeSet;
@@ -60,24 +64,22 @@
  */
 public class SimImportFragment extends DialogFragment
         implements LoaderManager.LoaderCallbacks<SimImportFragment.LoaderResult>,
-        MultiSelectEntryContactListAdapter.SelectedContactsListener {
+        MultiSelectEntryContactListAdapter.SelectedContactsListener, AbsListView.OnScrollListener {
 
-    private static final String KEY_SELECTED_IDS = "selectedIds";
+    private static final String KEY_SUFFIX_SELECTED_IDS = "_selectedIds";
     private static final String ARG_SUBSCRIPTION_ID = "subscriptionId";
 
-    public static final String CALLBACK_KEY_SUBSCRIPTION_ID = "simSubscriptionId";
-
     private ContactsPreferences mPreferences;
     private AccountTypeManager mAccountTypeManager;
     private SimContactAdapter mAdapter;
+    private View mAccountHeaderContainer;
     private AccountHeaderPresenter mAccountHeaderPresenter;
+    private float mAccountScrolledElevationPixels;
     private ContentLoadingProgressBar mLoadingIndicator;
     private Toolbar mToolbar;
     private ListView mListView;
     private View mImportButton;
 
-    private long[] mSelectedContacts;
-
     private int mSubscriptionId;
 
     @Override
@@ -98,9 +100,6 @@
         final Bundle args = getArguments();
         mSubscriptionId = args == null ? SimCard.NO_SUBSCRIPTION_ID :
                 args.getInt(ARG_SUBSCRIPTION_ID, SimCard.NO_SUBSCRIPTION_ID);
-
-        if (savedInstanceState == null) return;
-        mSelectedContacts = savedInstanceState.getLongArray(KEY_SELECTED_IDS);
     }
 
     @Override
@@ -123,8 +122,11 @@
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
         final View view = inflater.inflate(R.layout.fragment_sim_import, container, false);
 
+        mAccountHeaderContainer = view.findViewById(R.id.account_header_container);
+        mAccountScrolledElevationPixels = getResources()
+                .getDimension(R.dimen.contact_list_header_elevation);
         mAccountHeaderPresenter = new AccountHeaderPresenter(
-                view.findViewById(R.id.account_header_container));
+                mAccountHeaderContainer);
         if (savedInstanceState != null) {
             mAccountHeaderPresenter.onRestoreInstanceState(savedInstanceState);
         } else {
@@ -139,8 +141,10 @@
             }
         });
         mAdapter.setAccount(mAccountHeaderPresenter.getCurrentAccount());
+        restoreAdapterSelectedStates(savedInstanceState);
 
         mListView = (ListView) view.findViewById(R.id.list);
+        mListView.setOnScrollListener(this);
         mListView.setAdapter(mAdapter);
         mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
             @Override
@@ -162,7 +166,8 @@
                 dismiss();
             }
         });
-        mImportButton.setEnabled(mAdapter.getSelectedContactIds().size() > 0);
+        mImportButton.setVisibility(mAdapter.getSelectedContactIds().size() > 0 ?
+                View.VISIBLE : View.GONE);
 
         mToolbar = (Toolbar) view.findViewById(R.id.toolbar);
         mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@@ -190,9 +195,7 @@
     public void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
         mAccountHeaderPresenter.onSaveInstanceState(outState);
-        if (mAdapter != null && mAdapter.mContacts != null) {
-            outState.putLongArray(KEY_SELECTED_IDS, mAdapter.getSelectedContactIdsArray());
-        }
+        saveAdapterSelectedStates(outState);
     }
 
     @Override
@@ -209,23 +212,46 @@
             return;
         }
         mAdapter.setData(data);
-        if (mSelectedContacts != null) {
-            mAdapter.select(mSelectedContacts);
-        } else {
-            mAdapter.selectAll();
-        }
     }
 
     @Override
     public void onLoaderReset(Loader<LoaderResult> loader) {
     }
 
+    private void restoreAdapterSelectedStates(Bundle savedInstanceState) {
+        if (savedInstanceState == null) {
+            return;
+        }
+
+        final List<AccountWithDataSet> accounts = mAccountTypeManager.getAccounts(true);
+        for (AccountWithDataSet account : accounts) {
+            final long[] selections = savedInstanceState.getLongArray(
+                    account.stringify() + KEY_SUFFIX_SELECTED_IDS);
+            if (selections != null) {
+                mAdapter.setSelectionsForAccount(account, selections);
+            }
+        }
+    }
+
+    private void saveAdapterSelectedStates(Bundle outState) {
+        if (mAdapter == null) {
+            return;
+        }
+
+        // Make sure the selections are up-to-date
+        mAdapter.storeCurrentSelections();
+        for (Map.Entry<AccountWithDataSet, TreeSet<Long>> entry :
+                mAdapter.getSelectedIds().entrySet()) {
+            final long[] ids = Longs.toArray(entry.getValue());
+            outState.putLongArray(entry.getKey().stringify() + KEY_SUFFIX_SELECTED_IDS, ids);
+        }
+    }
+
     private void importCurrentSelections() {
-        final Bundle callbackData = new Bundle();
-        callbackData.putInt(CALLBACK_KEY_SUBSCRIPTION_ID, mSubscriptionId);
         ContactSaveService.startService(getContext(), ContactSaveService
-                .createImportFromSimIntent(getContext(), mAdapter.getSelectedContacts(),
-                mAccountHeaderPresenter.getCurrentAccount(), callbackData));
+                .createImportFromSimIntent(getContext(), mSubscriptionId,
+                        mAdapter.getSelectedContacts(),
+                        mAccountHeaderPresenter.getCurrentAccount()));
     }
 
     @Override
@@ -243,11 +269,10 @@
         if (selectedCount == 0) {
             mToolbar.setTitle(R.string.sim_import_title_none_selected);
         } else {
-            mToolbar.setTitle(getString(R.string.sim_import_title_some_selected_fmt,
-                    selectedCount));
+            mToolbar.setTitle(String.valueOf(selectedCount));
         }
         if (mImportButton != null) {
-            mImportButton.setEnabled(selectedCount > 0);
+            mImportButton.setVisibility(selectedCount > 0 ? View.VISIBLE : View.GONE);
         }
     }
 
@@ -258,6 +283,24 @@
         return getActivity();
     }
 
+    @Override
+    public void onScrollStateChanged(AbsListView view, int scrollState) { }
+
+    @Override
+    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+            int totalItemCount) {
+        int firstCompletelyVisibleItem = firstVisibleItem;
+        if (view != null && view.getChildAt(0) != null && view.getChildAt(0).getTop() < 0) {
+            firstCompletelyVisibleItem++;
+        }
+
+        if (firstCompletelyVisibleItem == 0) {
+            ViewCompat.setElevation(mAccountHeaderContainer, 0);
+        } else {
+            ViewCompat.setElevation(mAccountHeaderContainer, mAccountScrolledElevationPixels);
+        }
+    }
+
     /**
      * Creates a fragment that will display contacts stored on the default SIM card
      */
@@ -309,6 +352,7 @@
             mExistingMap = result.accountsMap;
             changeCursor(SimContact.convertToContactsCursor(mContacts,
                     ContactQuery.CONTACT_PROJECTION_PRIMARY));
+            updateDisplayedSelections();
         }
 
         public void setAccount(AccountWithDataSet account) {
@@ -318,11 +362,25 @@
             }
 
             // Save the checked state for the current account.
+            storeCurrentSelections();
+            mSelectedAccount = account;
+            updateDisplayedSelections();
+        }
+
+        public void storeCurrentSelections() {
             if (mSelectedAccount != null) {
                 mPerAccountCheckedIds.put(mSelectedAccount, getSelectedContactIds());
             }
+        }
 
-            mSelectedAccount = account;
+        public Map<AccountWithDataSet, TreeSet<Long>> getSelectedIds() {
+            return mPerAccountCheckedIds;
+        }
+
+        private void updateDisplayedSelections() {
+            if (mContacts == null) {
+                return;
+            }
 
             TreeSet<Long> checked = mPerAccountCheckedIds.get(mSelectedAccount);
             if (checked == null) {
@@ -347,24 +405,12 @@
             return selected;
         }
 
-        public void selectAll() {
-            if (mContacts == null) return;
-
-            final TreeSet<Long> selected = new TreeSet<>();
-            for (SimContact contact : mContacts) {
-                if (!existsInCurrentAccount(contact)) {
-                    selected.add(contact.getId());
-                }
+        public void setSelectionsForAccount(AccountWithDataSet account, long[] contacts) {
+            final TreeSet<Long> selected = new TreeSet<>(Longs.asList(contacts));
+            mPerAccountCheckedIds.put(account, selected);
+            if (account.equals(mSelectedAccount)) {
+                updateDisplayedSelections();
             }
-            setSelectedContactIds(selected);
-        }
-
-        public void select(long[] contacts) {
-            final TreeSet<Long> selected = new TreeSet<>();
-            for (long contact : contacts) {
-                selected.add(contact);
-            }
-            setSelectedContactIds(selected);
         }
 
         public boolean existsInCurrentAccount(int position) {
@@ -391,6 +437,12 @@
         private void setViewEnabled(ContactListItemView itemView, boolean enabled) {
             itemView.getCheckBox().setEnabled(enabled);
             itemView.getNameTextView().setEnabled(enabled);
+            // If the checkbox is left to default it's "unchecked" state will be announced when
+            // it is clicked on instead of the snackbar which is not useful.
+            int accessibilityMode = enabled ?
+                    View.IMPORTANT_FOR_ACCESSIBILITY_YES :
+                    View.IMPORTANT_FOR_ACCESSIBILITY_NO;
+            itemView.getCheckBox().setImportantForAccessibility(accessibilityMode);
         }
     }
 
diff --git a/src/com/android/contacts/activities/ContactEditorSpringBoardActivity.java b/src/com/android/contacts/activities/ContactEditorSpringBoardActivity.java
index 69dccb5..6394d07 100644
--- a/src/com/android/contacts/activities/ContactEditorSpringBoardActivity.java
+++ b/src/com/android/contacts/activities/ContactEditorSpringBoardActivity.java
@@ -1,12 +1,12 @@
 package com.android.contacts.activities;
 
+import android.app.Activity;
 import android.app.FragmentManager;
 import android.app.FragmentTransaction;
 import android.app.LoaderManager;
 import android.content.ContentUris;
 import android.content.Intent;
 import android.content.Loader;
-import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
 import android.provider.ContactsContract;
@@ -14,67 +14,68 @@
 import android.widget.Toast;
 
 import com.android.contacts.AppCompatContactsActivity;
+import com.android.contacts.ContactSaveService;
 import com.android.contacts.R;
 import com.android.contacts.common.activity.RequestPermissionsActivity;
+import com.android.contacts.common.logging.EditorEvent;
+import com.android.contacts.common.logging.Logger;
 import com.android.contacts.common.model.AccountTypeManager;
-import com.android.contacts.common.model.account.AccountType;
 import com.android.contacts.common.util.ImplicitIntentsUtil;
 import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette;
 import com.android.contacts.editor.ContactEditorFragment;
 import com.android.contacts.editor.EditorIntents;
 import com.android.contacts.editor.PickRawContactDialogFragment;
 import com.android.contacts.editor.PickRawContactLoader;
+import com.android.contacts.editor.PickRawContactLoader.RawContactsMetadata;
+import com.android.contacts.editor.SplitContactConfirmationDialogFragment;
+import com.android.contacts.quickcontact.QuickContactActivity;
+import com.android.contactsbind.FeedbackHelper;
 
 /**
  * Transparent springboard activity that hosts a dialog to select a raw contact to edit.
- * This activity has noHistory set to true, and all intents coming out from it have
- * {@code FLAG_ACTIVITY_FORWARD_RESULT} set.
+ * All intents coming out from this activity have {@code FLAG_ACTIVITY_FORWARD_RESULT} set.
  */
 public class ContactEditorSpringBoardActivity extends AppCompatContactsActivity implements
-        PickRawContactDialogFragment.PickRawContactListener {
+        PickRawContactDialogFragment.PickRawContactListener,
+        SplitContactConfirmationDialogFragment.Listener {
+
     private static final String TAG = "EditorSpringBoard";
     private static final String TAG_RAW_CONTACTS_DIALOG = "rawContactsDialog";
     private static final int LOADER_RAW_CONTACTS = 1;
 
+    public static final String EXTRA_SHOW_READ_ONLY = "showReadOnly";
+
     private Uri mUri;
-    private Cursor mCursor;
+    private RawContactsMetadata mResult;
     private MaterialPalette mMaterialPalette;
-    private boolean mIsUserProfile;
     private boolean mHasWritableAccount;
+    private boolean mShowReadOnly;
     private int mWritableAccountPosition;
 
     /**
      * The contact data loader listener.
      */
-    protected final LoaderManager.LoaderCallbacks<Cursor> mRawContactLoaderListener =
-            new LoaderManager.LoaderCallbacks<Cursor>() {
+    protected final LoaderManager.LoaderCallbacks<RawContactsMetadata> mRawContactLoaderListener =
+            new LoaderManager.LoaderCallbacks<RawContactsMetadata>() {
 
                 @Override
-                public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+                public Loader<RawContactsMetadata> onCreateLoader(int id, Bundle args) {
                     return new PickRawContactLoader(ContactEditorSpringBoardActivity.this, mUri);
                 }
 
                 @Override
-                public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
-                    if (cursor == null) {
-                        Toast.makeText(ContactEditorSpringBoardActivity.this,
-                                R.string.editor_failed_to_load, Toast.LENGTH_SHORT).show();
-                        finish();
+                public void onLoadFinished(Loader<RawContactsMetadata> loader,
+                        RawContactsMetadata result) {
+                    if (result == null) {
+                        toastErrorAndFinish();
                         return;
                     }
-                    mCursor = cursor;
-                    mIsUserProfile = ((PickRawContactLoader) loader).isUserProfile();
-                    setHasWritableAccount();
-                    if (mCursor.getCount() > 1 && mHasWritableAccount) {
-                        showDialog();
-                    } else {
-                        loadEditor();
-                    }
+                    mResult = result;
+                    onLoad();
                 }
 
                 @Override
-                public void onLoaderReset(Loader<Cursor> loader) {
-                    mCursor = null;
+                public void onLoaderReset(Loader<RawContactsMetadata> loader) {
                 }
             };
 
@@ -102,6 +103,7 @@
             mMaterialPalette = new MaterialPalette(intent.getIntExtra(primary, -1),
                     intent.getIntExtra(secondary, -1));
         }
+        mShowReadOnly = intent.getBooleanExtra(EXTRA_SHOW_READ_ONLY, false);
 
         mUri = intent.getData();
         final String authority = mUri.getAuthority();
@@ -109,8 +111,15 @@
         // Go straight to editor if we're passed a raw contact Uri.
         if (ContactsContract.AUTHORITY.equals(authority) &&
                 RawContacts.CONTENT_ITEM_TYPE.equals(type)) {
+            Logger.logEditorEvent(
+                    EditorEvent.EventType.SHOW_RAW_CONTACT_PICKER, /* numberRawContacts */ 0);
             final long rawContactId = ContentUris.parseId(mUri);
             startEditorAndForwardExtras(getIntentForRawContact(rawContactId));
+        } else if (android.provider.Contacts.AUTHORITY.equals(authority)) {
+            // Fail if given a legacy URI.
+            FeedbackHelper.sendFeedback(this, TAG,
+                    "Legacy Uri was passed to editor.", new IllegalArgumentException());
+            toastErrorAndFinish();
         } else {
             getLoaderManager().initLoader(LOADER_RAW_CONTACTS, null, mRawContactLoaderListener);
         }
@@ -122,25 +131,47 @@
     }
 
     /**
+     * Once the load is finished, decide whether to show the dialog or load the editor directly.
+     */
+    private void onLoad() {
+        maybeTrimReadOnly();
+        setHasWritableAccount();
+        if (mShowReadOnly || (mResult.rawContacts.size() > 1 && mHasWritableAccount)) {
+            showDialog();
+        } else {
+            loadEditor();
+        }
+    }
+
+    /**
+     * If not configured to show read only raw contact, trim them from the result.
+     */
+    private void maybeTrimReadOnly() {
+        mResult.showReadOnly = mShowReadOnly;
+        if (mShowReadOnly) {
+            return;
+        }
+
+        mResult.trimReadOnly(AccountTypeManager.getInstance(this));
+    }
+
+    /**
      * Start the dialog to pick the raw contact to edit.
      */
     private void showDialog() {
         final FragmentManager fm = getFragmentManager();
-        final PickRawContactDialogFragment oldFragment = (PickRawContactDialogFragment)
-                fm.findFragmentByTag(TAG_RAW_CONTACTS_DIALOG);
-        if (oldFragment != null && oldFragment.getDialog() != null
-                && oldFragment.getDialog().isShowing()) {
-            // Just update the cursor without reshowing the dialog.
-            oldFragment.setCursor(mCursor);
+        final SplitContactConfirmationDialogFragment split =
+                (SplitContactConfirmationDialogFragment) fm
+                        .findFragmentByTag(SplitContactConfirmationDialogFragment.TAG);
+        // If we were showing the split confirmation before show it again.
+        if (split != null && split.isAdded()) {
+            fm.beginTransaction().show(split).commitAllowingStateLoss();
             return;
         }
         final FragmentTransaction ft = fm.beginTransaction();
-        if (oldFragment != null) {
-            ft.remove(oldFragment);
-        }
-        final PickRawContactDialogFragment newFragment = PickRawContactDialogFragment.getInstance(
-                 mCursor, mIsUserProfile);
-        ft.add(newFragment, TAG_RAW_CONTACTS_DIALOG);
+        final PickRawContactDialogFragment pick = PickRawContactDialogFragment.getInstance(
+                 mResult);
+        ft.add(pick, TAG_RAW_CONTACTS_DIALOG);
         // commitAllowingStateLoss is safe in this activity because the fragment entirely depends
         // on the result of the loader. Even if we lose the fragment because the activity was
         // in the background, when it comes back onLoadFinished will be called again which will
@@ -154,39 +185,28 @@
      * the editor is started normally and handles creation of a new writable raw contact.
      */
     private void loadEditor() {
+        Logger.logEditorEvent(
+                EditorEvent.EventType.SHOW_RAW_CONTACT_PICKER, /* numberRawContacts */ 0);
         final Intent intent;
         if (mHasWritableAccount) {
-            mCursor.moveToPosition(mWritableAccountPosition);
-            final long rawContactId = mCursor.getLong(PickRawContactLoader.RAW_CONTACT_ID);
-            intent = getIntentForRawContact(rawContactId);
+            intent = getIntentForRawContact(mResult.rawContacts.get(mWritableAccountPosition).id);
         } else {
             // If the contact has only read-only raw contacts, we'll want to let the editor create
             // the writable raw contact for it.
             intent = EditorIntents.createEditContactIntent(this, mUri, mMaterialPalette, -1);
             intent.setClass(this, ContactEditorActivity.class);
         }
-        // Destroy the loader to prevent multiple onLoadFinished calls in case CP2 is updating in
-        // the background.
-        getLoaderManager().destroyLoader(LOADER_RAW_CONTACTS);
         startEditorAndForwardExtras(intent);
+        finish();
     }
 
     /**
      * Determines if this contact has a writable account.
      */
     private void setHasWritableAccount() {
-        mCursor.moveToPosition(-1);
-        while (mCursor.moveToNext()) {
-            final String accountType = mCursor.getString(PickRawContactLoader.ACCOUNT_TYPE);
-            final String dataSet = mCursor.getString(PickRawContactLoader.DATA_SET);
-            final AccountType account = AccountTypeManager.getInstance(this)
-                    .getAccountType(accountType, dataSet);
-            if (account.areContactsWritable()) {
-                mHasWritableAccount = true;
-                mWritableAccountPosition = mCursor.getPosition();
-                return;
-            }
-        }
+        mWritableAccountPosition = mResult.getIndexOfFirstWritableAccount(
+                AccountTypeManager.getInstance(this));
+        mHasWritableAccount = mWritableAccountPosition != -1;
     }
 
     /**
@@ -211,4 +231,47 @@
         }
         ImplicitIntentsUtil.startActivityInApp(this, intent);
     }
+
+    private void toastErrorAndFinish() {
+        Toast.makeText(ContactEditorSpringBoardActivity.this,
+                R.string.editor_failed_to_load, Toast.LENGTH_SHORT).show();
+        setResult(RESULT_CANCELED, null);
+        finish();
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        // Ignore failed requests
+        if (resultCode != Activity.RESULT_OK) {
+            finish();
+        }
+        if (data != null) {
+            final Intent intent = ContactSaveService.createJoinContactsIntent(
+                    this, mResult.contactId, ContentUris.parseId(data.getData()),
+                    QuickContactActivity.class, Intent.ACTION_VIEW);
+            startService(intent);
+            finish();
+        }
+    }
+
+    @Override
+    public void onSplitContactConfirmed(boolean hasPendingChanges) {
+        final long[][] rawContactIds = getRawContactIds();
+        final Intent intent = ContactSaveService.createHardSplitContactIntent(this, rawContactIds);
+        startService(intent);
+        finish();
+    }
+
+    @Override
+    public void onSplitContactCanceled() {
+        finish();
+    }
+
+    private long[][] getRawContactIds() {
+        final long[][] result = new long[mResult.rawContacts.size()][1];
+        for (int i = 0; i < mResult.rawContacts.size(); i++) {
+            result[i][0] = mResult.rawContacts.get(i).id;
+        }
+        return result;
+    }
 }
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index fd6ee89..5d478fe 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -107,7 +107,6 @@
     private View mFloatingActionButtonContainer;
     private boolean wasLastFabAnimationScaleIn = false;
 
-    private ContactsUnavailableFragment mContactsUnavailableFragment;
     private ProviderStatusWatcher mProviderStatusWatcher;
     private Integer mProviderStatus;
 
@@ -219,24 +218,6 @@
         return (mProviderStatus != null) && mProviderStatus.equals(ProviderStatus.STATUS_NORMAL);
     }
 
-    /**
-     * Initialize fragments that are (or may not be) in the layout.
-     *
-     * For the fragments that are in the layout, we initialize them in
-     * {@link #createViewsAndFragments()} after inflating the layout.
-     *
-     * However, the {@link ContactsUnavailableFragment} is a special fragment which may not
-     * be in the layout, so we have to do the initialization here.
-     *
-     * The ContactsUnavailableFragment is always created at runtime.
-     */
-    @Override
-    public void onAttachFragment(Fragment fragment) {
-        if (fragment instanceof ContactsUnavailableFragment) {
-            mContactsUnavailableFragment = (ContactsUnavailableFragment)fragment;
-        }
-    }
-
     @Override
     protected void onCreate(Bundle savedState) {
         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
@@ -449,10 +430,8 @@
 
         super.onPause();
 
-        if (Flags.getInstance().getBoolean(Experiments.PULL_TO_REFRESH)) {
-            ContentResolver.removeStatusChangeListener(mStatusChangeListenerHandle);
-            onSyncStateUpdated();
-        }
+        ContentResolver.removeStatusChangeListener(mStatusChangeListenerHandle);
+        onSyncStateUpdated();
     }
 
     @Override
@@ -471,14 +450,13 @@
         mProviderStatusWatcher.start();
         updateViewConfiguration(true);
 
-        if (Flags.getInstance().getBoolean(Experiments.PULL_TO_REFRESH)) {
-            mStatusChangeListenerHandle = ContentResolver.addStatusChangeListener(
-                    ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE
-                            | ContentResolver.SYNC_OBSERVER_TYPE_PENDING
-                            | ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS,
-                    mSyncStatusObserver);
-            onSyncStateUpdated();
-        }
+        mStatusChangeListenerHandle = ContentResolver.addStatusChangeListener(
+                ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE
+                        | ContentResolver.SYNC_OBSERVER_TYPE_PENDING
+                        | ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS,
+                mSyncStatusObserver);
+        onSyncStateUpdated();
+
         initializeFabVisibility();
         initializeHomeVisibility();
 
@@ -565,27 +543,27 @@
         // actually at least one real account (not "local" account) on device.
         if (shouldShowList()) {
             if (mAllFragment != null) {
-                transaction.show(mAllFragment);
+                final Fragment unavailableFragment = fragmentManager
+                        .findFragmentByTag(TAG_UNAVAILABLE);
+                if (unavailableFragment != null) {
+                    transaction.remove(unavailableFragment);
+                }
+                if (mAllFragment.isHidden()) {
+                    transaction.show(mAllFragment);
+                }
                 mAllFragment.setContactsAvailable(areContactsAvailable());
                 mAllFragment.setEnabled(true);
             }
-            if (mContactsUnavailableFragment != null) {
-                transaction.hide(mContactsUnavailableFragment);
-            }
         } else {
             // Setting up the page so that the user can still use the app
             // even without an account.
             if (mAllFragment != null) {
                 mAllFragment.setEnabled(false);
-                transaction.hide(mAllFragment);
             }
-            if (mContactsUnavailableFragment == null) {
-                mContactsUnavailableFragment = new ContactsUnavailableFragment();
-                transaction.add(R.id.contacts_list_container, mContactsUnavailableFragment,
-                        TAG_UNAVAILABLE);
-            }
-            transaction.show(mContactsUnavailableFragment);
-            mContactsUnavailableFragment.updateStatus(mProviderStatus);
+            final ContactsUnavailableFragment fragment = new ContactsUnavailableFragment();
+            transaction.hide(mAllFragment);
+            transaction.replace(R.id.contacts_unavailable_container, fragment, TAG_UNAVAILABLE);
+            fragment.updateStatus(mProviderStatus);
         }
         if (!transaction.isEmpty()) {
             transaction.commit();
@@ -831,29 +809,15 @@
             transaction.replace(
                     R.id.contacts_list_container, mMembersFragment, TAG_GROUP_VIEW);
         } else if (isAssistantView()) {
-            String fragmentTag;
-            if (Flags.getInstance().getBoolean(Experiments.ASSISTANT)) {
-                fragmentTag = TAG_ASSISTANT;
-            } else {
-                fragmentTag = TAG_DUPLICATES;
-            }
-
-            Fragment uiFragment = fragmentManager.findFragmentByTag(fragmentTag);
+            Fragment uiFragment = fragmentManager.findFragmentByTag(TAG_ASSISTANT);
+            Fragment unavailableFragment = fragmentManager.findFragmentByTag(TAG_UNAVAILABLE);
             if (uiFragment == null) {
-                uiFragment = ObjectFactory.getAssistantFragment(fragmentTag);
+                uiFragment = ObjectFactory.getAssistantFragment();
             }
-            transaction.replace(R.id.contacts_list_container, uiFragment, fragmentTag);
-
-            if (!Flags.getInstance().getBoolean(Experiments.ASSISTANT)) {
-                Fragment duplicatesUtilFragment =
-                        fragmentManager.findFragmentByTag(TAG_DUPLICATES_UTIL);
-                if (duplicatesUtilFragment == null) {
-                    duplicatesUtilFragment = ObjectFactory.getDuplicatesUtilFragment();
-                }
-                if (!duplicatesUtilFragment.isAdded()) {
-                    transaction.add(duplicatesUtilFragment, TAG_DUPLICATES_UTIL);
-                }
+            if (unavailableFragment != null) {
+                transaction.remove(unavailableFragment);
             }
+            transaction.replace(R.id.contacts_list_container, uiFragment, TAG_ASSISTANT);
             resetToolBarStatusBarColor();
         }
         transaction.addToBackStack(TAG_SECOND_LEVEL);
diff --git a/src/com/android/contacts/common/CallUtil.java b/src/com/android/contacts/common/CallUtil.java
index 88fca92..acfad52 100644
--- a/src/com/android/contacts/common/CallUtil.java
+++ b/src/com/android/contacts/common/CallUtil.java
@@ -20,6 +20,7 @@
 import com.android.contacts.common.compat.PhoneAccountSdkCompat;
 import com.android.contacts.common.util.PermissionsUtil;
 import com.android.contacts.common.util.PhoneNumberHelper;
+import com.android.contactsbind.FeedbackHelper;
 import com.android.phone.common.PhoneConstants;
 
 import android.content.Context;
@@ -41,6 +42,8 @@
  */
 public class CallUtil {
 
+    public static final String TAG = "CallUtil";
+
     /**
      * Indicates that the video calling is not available.
      */
@@ -140,26 +143,32 @@
             return VIDEO_CALLING_DISABLED;
         }
 
-        List<PhoneAccountHandle> accountHandles = telecommMgr.getCallCapablePhoneAccounts();
-        for (PhoneAccountHandle accountHandle : accountHandles) {
-            PhoneAccount account = telecommMgr.getPhoneAccount(accountHandle);
-            if (account != null) {
-                if (account.hasCapabilities(PhoneAccount.CAPABILITY_VIDEO_CALLING)) {
-                    // Builds prior to N do not have presence support.
-                    if (!CompatUtils.isVideoPresenceCompatible()) {
-                        return VIDEO_CALLING_ENABLED;
-                    }
+        try {
+            List<PhoneAccountHandle> accountHandles = telecommMgr.getCallCapablePhoneAccounts();
+            for (PhoneAccountHandle accountHandle : accountHandles) {
+                PhoneAccount account = telecommMgr.getPhoneAccount(accountHandle);
+                if (account != null) {
+                    if (account.hasCapabilities(PhoneAccount.CAPABILITY_VIDEO_CALLING)) {
+                        // Builds prior to N do not have presence support.
+                        if (!CompatUtils.isVideoPresenceCompatible()) {
+                            return VIDEO_CALLING_ENABLED;
+                        }
 
-                    int videoCapabilities = VIDEO_CALLING_ENABLED;
-                    if (account.hasCapabilities(
-                            PhoneAccountSdkCompat.CAPABILITY_VIDEO_CALLING_RELIES_ON_PRESENCE)) {
-                        videoCapabilities |= VIDEO_CALLING_PRESENCE;
+                        int videoCapabilities = VIDEO_CALLING_ENABLED;
+                        if (account.hasCapabilities(PhoneAccountSdkCompat
+                                .CAPABILITY_VIDEO_CALLING_RELIES_ON_PRESENCE)) {
+                            videoCapabilities |= VIDEO_CALLING_PRESENCE;
+                        }
+                        return videoCapabilities;
                     }
-                    return videoCapabilities;
                 }
             }
+            return VIDEO_CALLING_DISABLED;
+        } catch (SecurityException e) {
+            FeedbackHelper.sendFeedback(context, TAG,
+                    "Security exception when querying intent activities", e);
+            return VIDEO_CALLING_DISABLED;
         }
-        return VIDEO_CALLING_DISABLED;
     }
 
     /**
diff --git a/src/com/android/contacts/common/ContactPhotoManager.java b/src/com/android/contacts/common/ContactPhotoManager.java
index 5ec1eea..b9b4bcc 100644
--- a/src/com/android/contacts/common/ContactPhotoManager.java
+++ b/src/com/android/contacts/common/ContactPhotoManager.java
@@ -467,7 +467,7 @@
     /**
      * Calls {@link #loadThumbnail(ImageView, long, boolean, DefaultImageRequest,
      * DefaultImageProvider)} using the {@link DefaultImageProvider} {@link #DEFAULT_AVATAR}.
-    */
+     */
     public final void loadThumbnail(ImageView view, long photoId, boolean darkTheme,
             boolean isCircular, DefaultImageRequest defaultImageRequest) {
         loadThumbnail(view, photoId, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR);
@@ -830,7 +830,7 @@
         } else {
             if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoId);
             loadPhotoByIdOrUri(view, Request.createFromThumbnailId(photoId, darkTheme, isCircular,
-                    defaultProvider));
+                    defaultProvider, defaultImageRequest));
         }
     }
 
@@ -1178,6 +1178,8 @@
 
     /**
      * Stores the supplied bitmap in cache.
+     * bytes should be null to indicate a failure to load the photo. An empty byte[] signifies
+     * a successful load but no photo was available.
      */
     private void cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent) {
         if (DEBUG) {
@@ -1507,6 +1509,9 @@
                     while (cursor.moveToNext()) {
                         Long id = cursor.getLong(0);
                         byte[] bytes = cursor.getBlob(1);
+                        if (bytes == null) {
+                            bytes = new byte[0];
+                        }
                         cacheBitmap(id, bytes, preloading, -1);
                         mPhotoIds.remove(id);
                     }
@@ -1526,8 +1531,11 @@
                                 ContentUris.withAppendedId(Data.CONTENT_URI, id),
                                 COLUMNS, null, null, null);
                         if (profileCursor != null && profileCursor.moveToFirst()) {
-                            cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1),
-                                    preloading, -1);
+                            byte[] bytes = profileCursor.getBlob(1);
+                            if (bytes == null) {
+                                bytes = new byte[0];
+                            }
+                            cacheBitmap(profileCursor.getLong(0), bytes, preloading, -1);
                         } else {
                             // Couldn't load a photo this way either.
                             cacheBitmap(id, null, preloading, -1);
@@ -1641,9 +1649,9 @@
         }
 
         public static Request createFromThumbnailId(long id, boolean darkTheme, boolean isCircular,
-                DefaultImageProvider defaultProvider) {
+                DefaultImageProvider defaultProvider, DefaultImageRequest defaultRequest) {
             return new Request(id, null /* no URI */, -1, darkTheme, isCircular, defaultProvider,
-                    /* defaultRequest */ null);
+                    defaultRequest);
         }
 
         public static Request createFromUri(Uri uri, int requestedExtent, boolean darkTheme,
diff --git a/src/com/android/contacts/common/Experiments.java b/src/com/android/contacts/common/Experiments.java
index dee95dd..b182491 100644
--- a/src/com/android/contacts/common/Experiments.java
+++ b/src/com/android/contacts/common/Experiments.java
@@ -21,16 +21,6 @@
 public final class Experiments {
 
     /**
-     * Experiment to enable assistant in left navigation drawer.
-     */
-    public static final String ASSISTANT = "Assistant__enable_assistant";
-
-    /**
-     * Experiment to show the restore assistant on the assistants view.
-     */
-    public static final String ASSISTANT_RESTORE = "Assistant__restore";
-
-    /**
      * Whether to open contact sheet (aka smart profile) instead of our own QuickContact.
      */
     public static final String CONTACT_SHEET = "QuickContact__contact_sheet";
@@ -48,22 +38,12 @@
             "Shortcuts__dynamic_min_content_change_update_delay_millis";
 
     /**
-     * Experiment to enable dynamic strequent shortcuts.
-     */
-    public static final String DYNAMIC_SHORTCUTS = "Shortcuts__dynamic_shortcuts";
-
-    /**
      * Experiment to enable device account detection using CP2 queries
      */
     public static final String OEM_CP2_DEVICE_ACCOUNT_DETECTION_ENABLED =
             "OEM__cp2_device_account_detection_enabled";
 
     /**
-     * Experiment to toggle contacts sync using the pull to refresh gesture.
-     */
-    public static final String PULL_TO_REFRESH = "PullToRefresh__pull_to_refresh";
-
-    /**
      * Flags for maximum time to show spinner for a contacts sync.
      */
     public static final String PULL_TO_REFRESH_CANCEL_REFRESH_MILLIS =
@@ -79,11 +59,6 @@
      */
     public static final String SEARCH_YENTA_TIMEOUT_MILLIS = "Search__yenta_timeout";
 
-    /**
-     * The options for sending email/messages to groups and selections
-     */
-    public static final String SEND_TO_GROUP = "Groups__send_to_group";
-
     private Experiments() {
     }
 }
diff --git a/src/com/android/contacts/common/database/SimContactDao.java b/src/com/android/contacts/common/database/SimContactDao.java
index ee36645..3175ce4 100644
--- a/src/com/android/contacts/common/database/SimContactDao.java
+++ b/src/com/android/contacts/common/database/SimContactDao.java
@@ -24,7 +24,6 @@
 import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.net.Uri;
-import android.os.AsyncTask;
 import android.os.Build;
 import android.os.RemoteException;
 import android.provider.BaseColumns;
@@ -40,14 +39,12 @@
 import android.util.SparseArray;
 
 import com.android.contacts.R;
-import com.android.contacts.common.Experiments;
 import com.android.contacts.common.compat.CompatUtils;
 import com.android.contacts.common.model.SimCard;
 import com.android.contacts.common.model.SimContact;
 import com.android.contacts.common.model.account.AccountWithDataSet;
 import com.android.contacts.common.util.PermissionsUtil;
 import com.android.contacts.util.SharedPreferenceUtil;
-import com.android.contactsbind.experiments.Flags;
 import com.google.common.base.Joiner;
 
 import java.util.ArrayList;
@@ -71,6 +68,10 @@
     // to work on any phone.
     private static final int IMPORT_MAX_BATCH_SIZE = 300;
 
+    // How many SIM contacts to consider in a single query. This prevents hitting the SQLite
+    // query parameter limit.
+    static final int QUERY_MAX_BATCH_SIZE = 100;
+
     // Set to true for manual testing on an emulator or phone without a SIM card
     // DO NOT SUBMIT if set to true
     private static final boolean USE_FAKE_INSTANCE = false;
@@ -93,19 +94,6 @@
         mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
     }
 
-    public void warmupSimQueryIfNeeded() {
-        // Not needed if we don't have an Assistant section
-        if (!Flags.getInstance().getBoolean(Experiments.ASSISTANT) ||
-                !canReadSimContacts()) return;
-
-        new AsyncTask<Void, Void, Void>() {
-            @Override
-            protected Void doInBackground(Void... params) {
-                getSimCardsWithContacts();
-                return null;
-            }
-        }.execute();
-    }
 
     public Context getContext() {
         return mContext;
@@ -183,7 +171,7 @@
     }
 
     public SimCard getSimBySubscriptionId(int subscriptionId) {
-        final List<SimCard> sims = getSimCards();
+        final List<SimCard> sims = SharedPreferenceUtil.restoreSimStates(mContext, getSimCards());
         if (subscriptionId == SimCard.NO_SUBSCRIPTION_ID && !sims.isEmpty()) {
             return sims.get(0);
         }
@@ -198,9 +186,9 @@
     public Map<AccountWithDataSet, Set<SimContact>> findAccountsOfExistingSimContacts(
             List<SimContact> contacts) {
         final Map<AccountWithDataSet, Set<SimContact>> result = new ArrayMap<>();
-        for (int i = 0; i < contacts.size(); i += IMPORT_MAX_BATCH_SIZE) {
+        for (int i = 0; i < contacts.size(); i += QUERY_MAX_BATCH_SIZE) {
             findAccountsOfExistingSimContacts(
-                    contacts.subList(i, Math.min(contacts.size(), i + IMPORT_MAX_BATCH_SIZE)),
+                    contacts.subList(i, Math.min(contacts.size(), i + QUERY_MAX_BATCH_SIZE)),
                     result);
         }
         return result;
@@ -277,13 +265,19 @@
         return contacts != null ? contacts : loadContactsForSim(sim);
     }
 
+    // See b/32831092
+    // Sometimes the SIM contacts provider seems to get stuck if read from multiple threads
+    // concurrently. So we just have a global lock around it to prevent potential issues.
+    private static final Object SIM_READ_LOCK = new Object();
     private ArrayList<SimContact> loadFrom(Uri uri) {
-        final Cursor cursor = mResolver.query(uri, null, null, null, null);
+        synchronized (SIM_READ_LOCK) {
+            final Cursor cursor = mResolver.query(uri, null, null, null, null);
 
-        try {
-            return loadFromCursor(cursor);
-        } finally {
-            cursor.close();
+            try {
+                return loadFromCursor(cursor);
+            } finally {
+                cursor.close();
+            }
         }
     }
 
@@ -383,16 +377,16 @@
     public static SimContactDao create(Context context) {
         if (USE_FAKE_INSTANCE) {
             return new DebugImpl(context)
-                    .addSimCard(new SimCard("fake-sim-id1", 1, "Fake Carrier", "Card 1",
-                            "15095550101", "us").withContacts(
+                    .addSimCard(new SimCard("fake-sim-id1", 1, "Fake Carrier",
+                            "Card 1", "15095550101", "us").withContacts(
                             new SimContact(1, "Sim One", "15095550111", null),
                             new SimContact(2, "Sim Two", "15095550112", null),
                             new SimContact(3, "Sim Three", "15095550113", null),
                             new SimContact(4, "Sim Four", "15095550114", null),
                             new SimContact(5, "411 & more", "411", null)
                     ))
-                    .addSimCard(new SimCard("fake-sim-id2", 2, "Carrier Two", "Card 2",
-                            "15095550102", "us").withContacts(
+                    .addSimCard(new SimCard("fake-sim-id2", 2, "Carrier Two",
+                            "Card 2", "15095550102", "us").withContacts(
                             new SimContact(1, "John Sim", "15095550121", null),
                             new SimContact(2, "Bob Sim", "15095550122", null),
                             new SimContact(3, "Mary Sim", "15095550123", null),
@@ -405,7 +399,7 @@
 
     // TODO remove this class and the USE_FAKE_INSTANCE flag once this code is not under
     // active development or anytime after 3/1/2017
-    private static class DebugImpl extends SimContactDao {
+    public static class DebugImpl extends SimContactDao {
 
         private List<SimCard> mSimCards = new ArrayList<>();
         private SparseArray<SimCard> mCardsBySubscription = new SparseArray<>();
@@ -421,10 +415,6 @@
         }
 
         @Override
-        public void warmupSimQueryIfNeeded() {
-        }
-
-        @Override
         public List<SimCard> getSimCards() {
             return SharedPreferenceUtil.restoreSimStates(getContext(), mSimCards);
         }
@@ -447,9 +437,6 @@
 
     // Query used for detecting existing contacts that may match a SimContact.
     private static final class DataQuery {
-        // How many SIM contacts to consider in a single query. This prevents hitting the SQLite
-        // query parameter limit.
-        static final int MAX_BATCH_SIZE = 100;
 
         public static final String[] PROJECTION = new String[] {
                 ContactsContract.Data.RAW_CONTACT_ID, Phone.NUMBER, Phone.DISPLAY_NAME
diff --git a/src/com/android/contacts/common/interactions/ImportDialogFragment.java b/src/com/android/contacts/common/interactions/ImportDialogFragment.java
index f3ce748..a748b81 100644
--- a/src/com/android/contacts/common/interactions/ImportDialogFragment.java
+++ b/src/com/android/contacts/common/interactions/ImportDialogFragment.java
@@ -25,9 +25,6 @@
 import android.content.DialogInterface;
 import android.content.res.Resources;
 import android.os.Bundle;
-import android.support.v4.text.TextUtilsCompat;
-import android.text.SpannableString;
-import android.text.SpannableStringBuilder;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.LayoutInflater;
@@ -54,12 +51,11 @@
 /**
  * An dialog invoked to import/export contacts.
  */
-public class ImportDialogFragment extends DialogFragment
-        implements SelectAccountDialogFragment.Listener {
+public class ImportDialogFragment extends DialogFragment {
     public static final String TAG = "ImportDialogFragment";
 
-    private static final String KEY_RES_ID = "resourceId";
-    private static final String KEY_SUBSCRIPTION_ID = "subscriptionId";
+    public static final String KEY_RES_ID = "resourceId";
+    public static final String KEY_SUBSCRIPTION_ID = "subscriptionId";
 
     public static final String EXTRA_SIM_ONLY = "extraSimOnly";
 
@@ -95,7 +91,7 @@
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
-        setStyle(STYLE_NORMAL, R.style.ContactsAlertDialogThemeAppCompat);
+        setStyle(STYLE_NORMAL, R.style.ContactsAlertDialogTheme);
 
         final Bundle args = getArguments();
         mSimOnly = args != null && args.getBoolean(EXTRA_SIM_ONLY, false);
@@ -178,20 +174,16 @@
                 new DialogInterface.OnClickListener() {
             @Override
             public void onClick(DialogInterface dialog, int which) {
-                boolean dismissDialog;
                 final int resId = adapter.getItem(which).mChoiceResourceId;
                 if (resId == R.string.import_from_sim) {
-                    dismissDialog = handleSimImportRequest(adapter.getItem(which).mSim);
+                    handleSimImportRequest(adapter.getItem(which).mSim);
                 } else if (resId == R.string.import_from_vcf_file) {
-                        dismissDialog = handleImportRequest(resId, SimCard.NO_SUBSCRIPTION_ID);
+                    handleImportRequest(resId, SimCard.NO_SUBSCRIPTION_ID);
                 } else {
-                    dismissDialog = true;
                     Log.e(TAG, "Unexpected resource: "
                             + getActivity().getResources().getResourceEntryName(resId));
                 }
-                if (dismissDialog) {
-                    dialog.dismiss();
-                }
+                dialog.dismiss();
             }
         };
 
@@ -238,18 +230,15 @@
         }
     }
 
-    private boolean handleSimImportRequest(SimCard sim) {
+    private void handleSimImportRequest(SimCard sim) {
         SimImportFragment.newInstance(sim.getSubscriptionId()).show(getFragmentManager(),
                 "SimImport");
-        return true;
     }
 
     /**
      * Handle "import from SD".
-     *
-     * @return {@code true} if the dialog show be closed.  {@code false} otherwise.
      */
-    private boolean handleImportRequest(int resId, int subscriptionId) {
+    private void handleImportRequest(int resId, int subscriptionId) {
         // There are three possibilities:
         // - more than one accounts -> ask the user
         // - just one account -> use the account without asking the user
@@ -263,39 +252,13 @@
             args.putInt(KEY_RES_ID, resId);
             args.putInt(KEY_SUBSCRIPTION_ID, subscriptionId);
             SelectAccountDialogFragment.show(
-                    getFragmentManager(), this,
-                    R.string.dialog_new_contact_account,
+                    getFragmentManager(), R.string.dialog_new_contact_account,
                     AccountListFilter.ACCOUNTS_CONTACT_WRITABLE, args);
-
-            // In this case, because this DialogFragment is used as a target fragment to
-            // SelectAccountDialogFragment, we can't close it yet.  We close the dialog when
-            // we get a callback from it.
-            return false;
+        } else {
+            AccountSelectionUtil.doImport(getActivity(), resId,
+                    (size == 1 ? accountList.get(0) : null),
+                    (CompatUtils.isMSIMCompatible() ? subscriptionId : -1));
         }
-
-        AccountSelectionUtil.doImport(getActivity(), resId,
-                (size == 1 ? accountList.get(0) : null),
-                (CompatUtils.isMSIMCompatible() ? subscriptionId : -1));
-        return true; // Close the dialog.
-    }
-
-    /**
-     * Called when an account is selected on {@link SelectAccountDialogFragment}.
-     */
-    @Override
-    public void onAccountChosen(AccountWithDataSet account, Bundle extraArgs) {
-        AccountSelectionUtil.doImport(getActivity(), extraArgs.getInt(KEY_RES_ID),
-                account, extraArgs.getInt(KEY_SUBSCRIPTION_ID));
-
-        // At this point the dialog is still showing (which is why we can use getActivity() above)
-        // So close it.
-        dismiss();
-    }
-
-    @Override
-    public void onAccountSelectorCancelled() {
-        // See onAccountChosen() -- at this point the dialog is still showing.  Close it.
-        dismiss();
     }
 
     private CharSequence getSimDescription(SimCard sim, int index) {
diff --git a/src/com/android/contacts/common/list/AutoScrollListView.java b/src/com/android/contacts/common/list/AutoScrollListView.java
index ae7ca17..d0070b5 100644
--- a/src/com/android/contacts/common/list/AutoScrollListView.java
+++ b/src/com/android/contacts/common/list/AutoScrollListView.java
@@ -17,6 +17,7 @@
 package com.android.contacts.common.list;
 
 import android.content.Context;
+import android.os.Build;
 import android.util.AttributeSet;
 import android.widget.ListView;
 
@@ -114,4 +115,15 @@
             smoothScrollToPositionFromTop(position, offset);
         }
     }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+
+        // Workaround for b/31160338.
+        if (android.os.Build.VERSION.SDK_INT == Build.VERSION_CODES.N
+            || android.os.Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1) {
+            layoutChildren();
+        }
+    }
 }
diff --git a/src/com/android/contacts/common/list/ProviderStatusWatcher.java b/src/com/android/contacts/common/list/ProviderStatusWatcher.java
index 3e8e2eb..af36db3 100644
--- a/src/com/android/contacts/common/list/ProviderStatusWatcher.java
+++ b/src/com/android/contacts/common/list/ProviderStatusWatcher.java
@@ -27,6 +27,8 @@
 
 import com.android.contacts.common.compat.ProviderStatusCompat;
 
+import com.android.contactsbind.FeedbackHelper;
+
 import com.google.common.collect.Lists;
 
 import java.util.ArrayList;
@@ -231,6 +233,10 @@
                     }
                 }
                 return false;
+            } catch (SecurityException e) {
+                FeedbackHelper.sendFeedback(mContext, TAG,
+                        "Security exception when querying provider status", e);
+                return false;
             } finally {
                 synchronized (mSignal) {
                     mSignal.notifyAll();
diff --git a/src/com/android/contacts/common/logging/EditorEvent.java b/src/com/android/contacts/common/logging/EditorEvent.java
new file mode 100644
index 0000000..e5b42f8
--- /dev/null
+++ b/src/com/android/contacts/common/logging/EditorEvent.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016 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.common.logging;
+
+import com.google.common.base.MoreObjects;
+
+public class EditorEvent {
+
+    /** The editor event type that is logged. */
+    public int eventType;
+
+    /** The number of raw contacts shown in the raw contacts picker. */
+    public int numberRawContacts;
+
+    public static final class EventType {
+        public static final int UNKNOWN = 0;
+        public static final int SHOW_RAW_CONTACT_PICKER = 1;
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("eventType", eventType)
+                .add("numberRawContacts", numberRawContacts)
+                .toString();
+    }
+}
diff --git a/src/com/android/contacts/common/logging/Logger.java b/src/com/android/contacts/common/logging/Logger.java
index 68cbbbb..cb75ca9 100644
--- a/src/com/android/contacts/common/logging/Logger.java
+++ b/src/com/android/contacts/common/logging/Logger.java
@@ -96,8 +96,19 @@
         }
     }
 
+    public static void logEditorEvent(int eventType, int numberRawContacts) {
+        final Logger logger = getInstance();
+        if (logger != null) {
+            final EditorEvent event = new EditorEvent();
+            event.eventType = eventType;
+            event.numberRawContacts = numberRawContacts;
+            logger.logEditorEventImpl(event);
+        }
+    }
+
     public abstract void logScreenViewImpl(int screenType, int previousScreenType);
     public abstract void logSearchEventImpl(SearchState searchState);
     public abstract void logListEventImpl(ListEvent event);
     public abstract void logQuickContactEventImpl(QuickContactEvent event);
+    public abstract void logEditorEventImpl(EditorEvent event);
 }
diff --git a/src/com/android/contacts/common/model/AccountTypeManager.java b/src/com/android/contacts/common/model/AccountTypeManager.java
index 15c9771..f4cb428 100644
--- a/src/com/android/contacts/common/model/AccountTypeManager.java
+++ b/src/com/android/contacts/common/model/AccountTypeManager.java
@@ -45,6 +45,7 @@
 import android.util.TimingLogger;
 
 import com.android.contacts.R;
+import com.android.contacts.common.Experiments;
 import com.android.contacts.common.MoreContactUtils;
 import com.android.contacts.common.list.ContactListFilterController;
 import com.android.contacts.common.model.account.AccountType;
@@ -59,6 +60,7 @@
 import com.android.contacts.common.util.Constants;
 import com.android.contacts.common.util.DeviceLocalAccountTypeFactory;
 import com.android.contactsbind.ObjectFactory;
+import com.android.contactsbind.experiments.Flags;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Objects;
 import com.google.common.base.Predicate;
@@ -464,26 +466,29 @@
 
         ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this);
 
-        // Observe changes to RAW_CONTACTS so that we will update the list of "Device" accounts
-        // if a new device contact is added.
-        mContext.getContentResolver().registerContentObserver(
-                ContactsContract.RawContacts.CONTENT_URI, /* notifyDescendents */ true,
-                new ContentObserver(mListenerHandler) {
-            @Override
-            public boolean deliverSelfNotifications() {
-                return true;
-            }
 
-            @Override
-            public void onChange(boolean selfChange) {
-                mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
-            }
+        if (Flags.getInstance().getBoolean(Experiments.OEM_CP2_DEVICE_ACCOUNT_DETECTION_ENABLED)) {
+            // Observe changes to RAW_CONTACTS so that we will update the list of "Device" accounts
+            // if a new device contact is added.
+            mContext.getContentResolver().registerContentObserver(
+                    ContactsContract.RawContacts.CONTENT_URI, /* notifyDescendents */ true,
+                    new ContentObserver(mListenerHandler) {
+                        @Override
+                        public boolean deliverSelfNotifications() {
+                            return true;
+                        }
 
-            @Override
-            public void onChange(boolean selfChange, Uri uri) {
-                mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
-            }
-        });
+                        @Override
+                        public void onChange(boolean selfChange) {
+                            mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
+                        }
+
+                        @Override
+                        public void onChange(boolean selfChange, Uri uri) {
+                            mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
+                        }
+                    });
+        }
 
         mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
     }
diff --git a/src/com/android/contacts/common/model/SimCard.java b/src/com/android/contacts/common/model/SimCard.java
index c316ec4..7b13096 100644
--- a/src/com/android/contacts/common/model/SimCard.java
+++ b/src/com/android/contacts/common/model/SimCard.java
@@ -118,6 +118,18 @@
         return mCountryCode;
     }
 
+    /**
+     * Returns whether the contacts for this SIM card have been initialized.
+     */
+    public boolean areContactsAvailable() {
+        return mContacts != null;
+    }
+
+    /**
+     * Returns whether this SIM card has any SIM contacts.
+     *
+     * A precondition of this method is that the contacts have been initialized.
+     */
     public boolean hasContacts() {
         if (mContacts == null) {
             throw new IllegalStateException("Contacts not loaded.");
@@ -125,6 +137,11 @@
         return !mContacts.isEmpty();
     }
 
+    /**
+     * Returns the number of contacts stored on this SIM card.
+     *
+     * A precondition of this method is that the contacts have been initialized.
+     */
     public int getContactCount() {
         if (mContacts == null) {
             throw new IllegalStateException("Contacts not loaded.");
@@ -148,6 +165,9 @@
         return !isDismissed() && !isImported() && hasContacts();
     }
 
+    /**
+     * Returns the contacts for this SIM card or null if the contacts have not been initialized.
+     */
     public List<SimContact> getContacts() {
         return mContacts;
     }
@@ -164,7 +184,7 @@
     }
 
     public SimCard withDismissedState(boolean dismissed) {
-        return withImportAndDismissStates(dismissed, mImported);
+        return withImportAndDismissStates(mImported, dismissed);
     }
 
     public SimCard withContacts(List<SimContact> contacts) {
diff --git a/src/com/android/contacts/common/model/account/BaseAccountType.java b/src/com/android/contacts/common/model/account/BaseAccountType.java
index 5ed79ef..400b0e9 100644
--- a/src/com/android/contacts/common/model/account/BaseAccountType.java
+++ b/src/com/android/contacts/common/model/account/BaseAccountType.java
@@ -187,24 +187,24 @@
                 context.getResources().getBoolean(R.bool.config_editor_field_order_primary);
 
         kind.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
-                FLAGS_PERSON_NAME).setLongForm(true));
+                FLAGS_PERSON_NAME).setOptional(true));
         if (!displayOrderPrimary) {
             kind.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
                     FLAGS_PERSON_NAME));
             kind.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
-                    FLAGS_PERSON_NAME).setLongForm(true));
+                    FLAGS_PERSON_NAME).setOptional(true));
             kind.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
                     FLAGS_PERSON_NAME));
         } else {
             kind.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
                     FLAGS_PERSON_NAME));
             kind.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
-                    FLAGS_PERSON_NAME).setLongForm(true));
+                    FLAGS_PERSON_NAME).setOptional(true));
             kind.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
                     FLAGS_PERSON_NAME));
         }
         kind.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
-                FLAGS_PERSON_NAME).setLongForm(true));
+                FLAGS_PERSON_NAME).setOptional(true));
 
         return kind;
     }
@@ -947,24 +947,24 @@
             kinds.add(kn);
 
             kn.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
-                    FLAGS_PERSON_NAME).setLongForm(true));
+                    FLAGS_PERSON_NAME).setOptional(true));
             if (!displayOrderPrimary) {
                 kn.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
                         FLAGS_PERSON_NAME));
                 kn.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
-                        FLAGS_PERSON_NAME).setLongForm(true));
+                        FLAGS_PERSON_NAME).setOptional(true));
                 kn.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
                         FLAGS_PERSON_NAME));
             } else {
                 kn.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
                         FLAGS_PERSON_NAME));
                 kn.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
-                        FLAGS_PERSON_NAME).setLongForm(true));
+                        FLAGS_PERSON_NAME).setOptional(true));
                 kn.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
                         FLAGS_PERSON_NAME));
             }
             kn.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
-                    FLAGS_PERSON_NAME).setLongForm(true));
+                    FLAGS_PERSON_NAME).setOptional(true));
 
             // Phonetic name
             final DataKind kp = newDataKind(context, parser, attrs, true,
diff --git a/src/com/android/contacts/common/preference/ContactsPreferenceActivity.java b/src/com/android/contacts/common/preference/ContactsPreferenceActivity.java
index 030575d..4bdb12f 100644
--- a/src/com/android/contacts/common/preference/ContactsPreferenceActivity.java
+++ b/src/com/android/contacts/common/preference/ContactsPreferenceActivity.java
@@ -34,15 +34,19 @@
 import android.view.ViewGroup;
 
 import com.android.contacts.common.R;
+import com.android.contacts.common.interactions.ImportDialogFragment;
 import com.android.contacts.common.list.ProviderStatusWatcher;
+import com.android.contacts.common.model.account.AccountWithDataSet;
 import com.android.contacts.common.preference.DisplayOptionsPreferenceFragment.ProfileListener;
 import com.android.contacts.common.preference.DisplayOptionsPreferenceFragment.ProfileQuery;
+import com.android.contacts.common.util.AccountSelectionUtil;
+import com.android.contacts.editor.SelectAccountDialogFragment;
 
 /**
  * Contacts settings.
  */
-public final class ContactsPreferenceActivity extends PreferenceActivity implements
-        ProfileListener {
+public final class ContactsPreferenceActivity extends PreferenceActivity
+        implements ProfileListener, SelectAccountDialogFragment.Listener {
 
     private static final String TAG_ABOUT = "about_contacts";
     private static final String TAG_DISPLAY_OPTIONS = "display_options";
@@ -210,4 +214,14 @@
                 getFragmentManager().findFragmentByTag(TAG_DISPLAY_OPTIONS);
         fragment.updateMyInfoPreference(hasProfile, displayName, contactId);
     }
+
+    @Override
+    public void onAccountChosen(AccountWithDataSet account, Bundle extraArgs) {
+        AccountSelectionUtil.doImport(this, extraArgs.getInt(ImportDialogFragment
+                .KEY_RES_ID), account, extraArgs.getInt(ImportDialogFragment.KEY_SUBSCRIPTION_ID));
+    }
+
+    @Override
+    public void onAccountSelectorCancelled() {
+    }
 }
diff --git a/src/com/android/contacts/common/preference/ContactsPreferences.java b/src/com/android/contacts/common/preference/ContactsPreferences.java
index 8865e91..ac7b0e0 100644
--- a/src/com/android/contacts/common/preference/ContactsPreferences.java
+++ b/src/com/android/contacts/common/preference/ContactsPreferences.java
@@ -16,13 +16,10 @@
 
 package com.android.contacts.common.preference;
 
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.content.SharedPreferences.Editor;
 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
-import android.net.Uri;
-import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
 import android.preference.PreferenceManager;
@@ -219,7 +216,8 @@
             return defaultAccount == null || !defaultAccount.isNullAccount();
         }
 
-        if (currentWritableAccounts.size() == 1) {
+        if (currentWritableAccounts.size() == 1
+                && !currentWritableAccounts.get(0).isNullAccount()) {
             return false;
         }
 
diff --git a/src/com/android/contacts/editor/AccountHeaderPresenter.java b/src/com/android/contacts/editor/AccountHeaderPresenter.java
index 5323c60..d4f613b 100644
--- a/src/com/android/contacts/editor/AccountHeaderPresenter.java
+++ b/src/com/android/contacts/editor/AccountHeaderPresenter.java
@@ -76,6 +76,8 @@
     public AccountHeaderPresenter(View container) {
         mContext = container.getContext();
         mAccountHeaderContainer = container;
+        // mAccountHeaderType is optional and may not be in the container view in which case
+        // the variable will be null
         mAccountHeaderType = (TextView) container.findViewById(R.id.account_type);
         mAccountHeaderName = (TextView) container.findViewById(R.id.account_name);
         mAccountHeaderIcon = (ImageView) container.findViewById(R.id.account_type_icon);
@@ -144,7 +146,9 @@
 
         // Set the account type
         final String selectorTitle = mContext.getResources().getString(mSelectorTitle);
-        mAccountHeaderType.setText(selectorTitle);
+        if (mAccountHeaderType != null) {
+            mAccountHeaderType.setText(selectorTitle);
+        }
 
         // Set the icon
         final AccountDisplayInfo displayInfo = mAccountDisplayInfoFactory
diff --git a/src/com/android/contacts/editor/AggregationSuggestionEngine.java b/src/com/android/contacts/editor/AggregationSuggestionEngine.java
index 62e55e5..4c45db8 100644
--- a/src/com/android/contacts/editor/AggregationSuggestionEngine.java
+++ b/src/com/android/contacts/editor/AggregationSuggestionEngine.java
@@ -40,8 +40,7 @@
 import com.android.contacts.common.model.ValuesDelta;
 import com.android.contacts.common.model.account.AccountWithDataSet;
 import com.android.contacts.compat.AggregationSuggestionsCompat;
-
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.collect.Lists;
 
 import java.util.ArrayList;
@@ -62,6 +61,7 @@
         public long contactId;
         public String contactLookupKey;
         public long rawContactId;
+        public long photoId;
         public String name;
         public String phoneNumber;
         public String emailAddress;
@@ -69,10 +69,11 @@
 
         @Override
         public String toString() {
-            return Objects.toStringHelper(Suggestion.class)
+            return MoreObjects.toStringHelper(Suggestion.class)
                     .add("contactId", contactId)
                     .add("contactLookupKey", contactLookupKey)
                     .add("rawContactId", rawContactId)
+                    .add("photoId", photoId)
                     .add("name", name)
                     .add("phoneNumber", phoneNumber)
                     .add("emailAddress", emailAddress)
@@ -285,7 +286,8 @@
                 Data.IS_SUPER_PRIMARY,
                 RawContacts.ACCOUNT_TYPE,
                 RawContacts.ACCOUNT_NAME,
-                RawContacts.DATA_SET
+                RawContacts.DATA_SET,
+                Contacts.PHOTO_ID
         };
 
         public static final int CONTACT_ID = 0;
@@ -297,6 +299,7 @@
         public static final int ACCOUNT_TYPE = 6;
         public static final int ACCOUNT_NAME = 7;
         public static final int DATA_SET = 8;
+        public static final int PHOTO_ID = 9;
     }
 
     private void loadAggregationSuggestions(Uri uri) {
@@ -387,6 +390,7 @@
                 if (rawContactId != currentRawContactId) {
                     suggestion = new Suggestion();
                     suggestion.rawContactId = rawContactId;
+                    suggestion.photoId = mDataCursor.getLong(DataQuery.PHOTO_ID);
                     suggestion.contactId = mDataCursor.getLong(DataQuery.CONTACT_ID);
                     suggestion.contactLookupKey = mDataCursor.getString(DataQuery.LOOKUP_KEY);
                     final String accountName = mDataCursor.getString(DataQuery.ACCOUNT_NAME);
diff --git a/src/com/android/contacts/editor/AggregationSuggestionView.java b/src/com/android/contacts/editor/AggregationSuggestionView.java
index b89f06a..42db014 100644
--- a/src/com/android/contacts/editor/AggregationSuggestionView.java
+++ b/src/com/android/contacts/editor/AggregationSuggestionView.java
@@ -16,10 +16,8 @@
 
 package com.android.contacts.editor;
 
-import android.content.ContentUris;
 import android.content.Context;
 import android.net.Uri;
-import android.provider.ContactsContract;
 import android.provider.ContactsContract.Contacts;
 import android.text.TextUtils;
 import android.util.AttributeSet;
@@ -65,21 +63,18 @@
         final ContactPhotoManager.DefaultImageRequest
                 request = new ContactPhotoManager.DefaultImageRequest(
                 suggestion.name, String.valueOf(suggestion.rawContactId), /* isCircular = */ false);
-        final Uri photoUri = Uri.withAppendedPath(ContentUris.withAppendedId(
-                ContactsContract.RawContacts.CONTENT_URI, suggestion.rawContactId),
-                ContactsContract.RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
         final ImageView photoView = (ImageView) findViewById(
                 R.id.aggregation_suggestion_photo);
-        ContactPhotoManager.getInstance(getContext()).loadDirectoryPhoto(photoView,
-                photoUri,
+        ContactPhotoManager.getInstance(getContext()).loadThumbnail(photoView,
+                suggestion.photoId,
                 /* darkTheme = */ false,
                 /* isCircular = */ false,
                 request);
 
-        TextView name = (TextView) findViewById(R.id.aggregation_suggestion_name);
+        final TextView name = (TextView) findViewById(R.id.aggregation_suggestion_name);
         name.setText(suggestion.name);
 
-        TextView data = (TextView) findViewById(R.id.aggregation_suggestion_data);
+        final TextView data = (TextView) findViewById(R.id.aggregation_suggestion_data);
         String dataText = null;
         if (suggestion.nickname != null) {
             dataText = suggestion.nickname;
diff --git a/src/com/android/contacts/editor/ContactEditorFragment.java b/src/com/android/contacts/editor/ContactEditorFragment.java
index 65c30e8..384c163 100644
--- a/src/com/android/contacts/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorFragment.java
@@ -17,6 +17,7 @@
 package com.android.contacts.editor;
 
 import android.accounts.Account;
+import android.app.ActionBar;
 import android.app.Activity;
 import android.app.Fragment;
 import android.app.LoaderManager;
@@ -330,6 +331,7 @@
     //
     // Used to store existing contact data so it can be re-applied during a rebind call,
     // i.e. account switch.
+    protected Contact mContact;
     protected ImmutableList<RawContact> mRawContacts;
     protected Cursor mGroupMetaData;
 
@@ -797,6 +799,9 @@
         save(SaveMode.SPLIT);
     }
 
+    @Override
+    public void onSplitContactCanceled() {}
+
     private boolean doSplitContactAction() {
         if (!hasValidState()) return false;
 
@@ -987,6 +992,9 @@
             mStatus = Status.SUB_ACTIVITY;
             startActivityForResult(intent, REQUEST_CODE_ACCOUNTS_CHANGED);
         } else {
+            // Make sure the default account is automatically set if there is only one non-device
+            // account.
+            mEditorUtils.maybeUpdateDefaultAccount();
             // Otherwise, there should be a default account. Then either create a local contact
             // (if default account is null) or create a contact with the specified account.
             AccountWithDataSet defaultAccount = mEditorUtils.getOnlyOrDefaultAccount();
@@ -1034,6 +1042,7 @@
             Log.v(TAG, "Ignoring background change. This will have to be rebased later");
             return;
         }
+        mContact = contact;
         mRawContacts = contact.getRawContacts();
 
         // Check for writable raw contacts.  If there are none, then we need to create one so user
@@ -1210,7 +1219,7 @@
         }
         final int writableIndex = mState.indexOfFirstWritableRawContact(getContext());
         final RawContactDelta writable = mState.get(writableIndex);
-        final RawContactDelta readOnly = mState.get(writableIndex == 0 ? 1 : 0);
+        final RawContactDelta readOnly = mState.getByRawContactId(mContact.getNameRawContactId());
         final ValuesDelta writeNameDelta = writable
                 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
         final ValuesDelta readNameDelta = readOnly
@@ -1237,6 +1246,13 @@
         editorView.setState(mState, mMaterialPalette, mViewIdGenerator,
                 mHasNewContact, mIsUserProfile, mAccountWithDataSet,
                 mRawContactIdToDisplayAlone);
+        if (isEditingReadOnlyRawContact()) {
+            final ActionBar actionBar = getEditorActivity().getActionBar();
+            if (actionBar != null) {
+                actionBar.setTitle(R.string.contact_editor_title_read_only_contact);
+                actionBar.setHomeAsUpIndicator(R.drawable.ic_back_arrow);
+            }
+        }
 
         // Set up the photo widget
         editorView.setPhotoListener(this);
diff --git a/src/com/android/contacts/editor/ContactEditorUtils.java b/src/com/android/contacts/editor/ContactEditorUtils.java
index fc1a887..5b80b04 100644
--- a/src/com/android/contacts/editor/ContactEditorUtils.java
+++ b/src/com/android/contacts/editor/ContactEditorUtils.java
@@ -132,6 +132,20 @@
         return mContactsPrefs.shouldShowAccountChangedNotification(getWritableAccounts());
     }
 
+    /**
+     * Sets the only non-device account to be default if it is not already.
+     */
+    public void maybeUpdateDefaultAccount() {
+        final List<AccountWithDataSet> currentWritableAccounts = getWritableAccounts();
+        if (currentWritableAccounts.size() == 1) {
+            final AccountWithDataSet onlyAccount = currentWritableAccounts.get(0);
+            if (!onlyAccount.isNullAccount()
+                    && !onlyAccount.equals(mContactsPrefs.getDefaultAccount())) {
+                mContactsPrefs.setDefaultAccount(onlyAccount);
+            }
+        }
+    }
+
     @VisibleForTesting
     String[] getWritableAccountTypeStrings() {
         final Set<String> types = Sets.newHashSet();
diff --git a/src/com/android/contacts/editor/EditorIntents.java b/src/com/android/contacts/editor/EditorIntents.java
index 015e75b..b867b31 100644
--- a/src/com/android/contacts/editor/EditorIntents.java
+++ b/src/com/android/contacts/editor/EditorIntents.java
@@ -25,7 +25,6 @@
 
 import com.android.contacts.activities.ContactEditorActivity;
 import com.android.contacts.activities.ContactEditorSpringBoardActivity;
-import com.android.contacts.activities.ContactSelectionActivity;
 import com.android.contacts.common.model.RawContactDeltaList;
 import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette;
 
@@ -52,6 +51,15 @@
         return intent;
     }
 
+    public static Intent createViewLinkedContactsIntent(Context context, Uri uri,
+            MaterialPalette materialPalette) {
+        final Intent intent = createEditContactIntent(context, uri, materialPalette,
+                /* photoId */ -1);
+        intent.putExtra(ContactEditorSpringBoardActivity.EXTRA_SHOW_READ_ONLY, true);
+
+        return intent;
+    }
+
     /**
      * Returns an Intent to start the {@link ContactEditorActivity} for the given raw contact.
      */
@@ -100,18 +108,6 @@
         return intent;
     }
 
-    /**
-     * Returns an Intent to start the {@link ContactSelectionActivity} for a
-     * new or existing contact.
-     */
-    public static Intent createInsertOrEditContactIntent(Context context,
-            RawContactDeltaList rawContactDeltaList, String displayName, String phoneticName) {
-        final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT, Contacts.CONTENT_URI,
-                context, ContactSelectionActivity.class);
-        putRawContactDeltaValues(intent, rawContactDeltaList, displayName, phoneticName);
-        return intent;
-    }
-
     private static void putMaterialPalette(Intent intent, MaterialPalette materialPalette) {
         if (materialPalette != null) {
             intent.putExtra(
diff --git a/src/com/android/contacts/editor/PickRawContactDialogFragment.java b/src/com/android/contacts/editor/PickRawContactDialogFragment.java
index 6b0051e..23a2b9b 100644
--- a/src/com/android/contacts/editor/PickRawContactDialogFragment.java
+++ b/src/com/android/contacts/editor/PickRawContactDialogFragment.java
@@ -3,23 +3,24 @@
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.app.DialogFragment;
-import android.content.ContentUris;
 import android.content.Context;
 import android.content.DialogInterface;
-import android.database.Cursor;
-import android.net.Uri;
+import android.content.Intent;
 import android.os.Bundle;
-import android.provider.ContactsContract.RawContacts;
 import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.CursorAdapter;
+import android.widget.BaseAdapter;
 import android.widget.ImageView;
+import android.widget.ListAdapter;
 import android.widget.TextView;
 
 import com.android.contacts.R;
+import com.android.contacts.activities.ContactSelectionActivity;
 import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.logging.EditorEvent;
+import com.android.contacts.common.logging.Logger;
 import com.android.contacts.common.model.AccountTypeManager;
 import com.android.contacts.common.model.account.AccountDisplayInfo;
 import com.android.contacts.common.model.account.AccountDisplayInfoFactory;
@@ -27,6 +28,9 @@
 import com.android.contacts.common.model.account.AccountWithDataSet;
 import com.android.contacts.common.model.account.GoogleAccountType;
 import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.editor.PickRawContactLoader.RawContact;
+import com.android.contacts.editor.PickRawContactLoader.RawContactsMetadata;
+import com.android.contacts.list.UiIntentActions;
 
 /**
  * Should only be started from an activity that implements {@link PickRawContactListener}.
@@ -34,7 +38,8 @@
  * for the chosen raw contact.
  */
 public class PickRawContactDialogFragment extends DialogFragment {
-    private static final String ARGS_IS_USER_PROFILE = "isUserProfile";
+    private static final String ARGS_RAW_CONTACTS_METADATA = "rawContactsMetadata";
+    private static final int REQUEST_CODE_JOIN = 3;
 
     public interface PickRawContactListener {
         void onPickRawContact(long rawContactId);
@@ -43,96 +48,100 @@
     /**
      * Used to list the account info for the given raw contacts list.
      */
-    private final class RawContactAccountListAdapter extends CursorAdapter {
+    private final class RawContactAccountListAdapter extends BaseAdapter {
         private final LayoutInflater mInflater;
         private final Context mContext;
+        private final RawContactsMetadata mRawContactsMetadata;
         private final AccountDisplayInfoFactory mAccountDisplayInfoFactory;
         private final AccountTypeManager mAccountTypeManager;
         private final ContactsPreferences mPreferences;
 
-        public RawContactAccountListAdapter(Context context, Cursor cursor) {
-            super(context, cursor, 0);
+        public RawContactAccountListAdapter(Context context,
+                RawContactsMetadata rawContactsMetadata) {
             mContext = context;
             mInflater = LayoutInflater.from(context);
             mAccountDisplayInfoFactory = AccountDisplayInfoFactory.forWritableAccounts(context);
             mAccountTypeManager = AccountTypeManager.getInstance(context);
             mPreferences = new ContactsPreferences(context);
+            mRawContactsMetadata = rawContactsMetadata;
         }
 
         @Override
-        public void bindView(View view, Context context, Cursor cursor) {
-            final long rawContactId = cursor.getLong(PickRawContactLoader.RAW_CONTACT_ID);
-            final String accountName = cursor.getString(PickRawContactLoader.ACCOUNT_NAME);
-            final String accountType = cursor.getString(PickRawContactLoader.ACCOUNT_TYPE);
-            final String dataSet = cursor.getString(PickRawContactLoader.DATA_SET);
-            final AccountType account = mAccountTypeManager.getAccountType(accountType, dataSet);
+        public int getCount() {
+            return mRawContactsMetadata.rawContacts.size();
+        }
 
-            final int displayNameColumn =
+        @Override
+        public Object getItem(int position) {
+            return mRawContactsMetadata.rawContacts.get(position);
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return mRawContactsMetadata.rawContacts.get(position).id;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            final View view;
+            final RawContactViewHolder holder;
+            if (convertView == null) {
+                view = mInflater.inflate(R.layout.raw_contact_list_item, parent, false);
+                holder = new RawContactViewHolder();
+                holder.displayName = (TextView) view.findViewById(R.id.display_name);
+                holder.accountName = (TextView) view.findViewById(R.id.account_name);
+                holder.accountIcon = (ImageView) view.findViewById(R.id.account_icon);
+                holder.photo = (ImageView) view.findViewById(R.id.photo);
+                view.setTag(holder);
+            } else {
+                view = convertView;
+                holder = (RawContactViewHolder) view.getTag();
+            }
+            final RawContact rawContact = mRawContactsMetadata.rawContacts.get(position);
+            final AccountType account = mAccountTypeManager.getAccountType(rawContact.accountType,
+                    rawContact.accountDataSet);
+
+            String displayName =
                     mPreferences.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY
-                            ? PickRawContactLoader.DISPLAY_NAME_PRIMARY
-                            : PickRawContactLoader.DISPLAY_NAME_ALTERNATIVE;
-
-            String displayName = cursor.getString(displayNameColumn);
+                            ? rawContact.displayName : rawContact.displayNameAlt;
 
             if (TextUtils.isEmpty(displayName)) {
                 displayName = mContext.getString(R.string.missing_name);
             }
-            final RawContactViewHolder holder = (RawContactViewHolder) view.getTag();
             holder.displayName.setText(displayName);
 
             final String accountDisplayLabel;
 
             // Use the same string as editor if it's an editable user profile raw contact.
-            if (mIsUserProfile && account.areContactsWritable()) {
+            if (mRawContactsMetadata.isUserProfile && account.areContactsWritable()) {
                 final AccountDisplayInfo displayInfo =
                         mAccountDisplayInfoFactory.getAccountDisplayInfo(
-                                new AccountWithDataSet(accountName, accountType, dataSet));
+                                new AccountWithDataSet(rawContact.accountName,
+                                        rawContact.accountType, rawContact.accountDataSet));
                 accountDisplayLabel = EditorUiUtils.getAccountHeaderLabelForMyProfile(mContext,
                         displayInfo);
-            }
-            else if (GoogleAccountType.ACCOUNT_TYPE.equals(accountType)
+            } else if (GoogleAccountType.ACCOUNT_TYPE.equals(rawContact.accountType)
                     && account.dataSet == null) {
                 // Focus Google accounts have the account name shown
-                accountDisplayLabel = accountName;
+                accountDisplayLabel = rawContact.accountName;
             } else {
                 accountDisplayLabel = account.getDisplayLabel(mContext).toString();
             }
 
             holder.accountName.setText(accountDisplayLabel);
             holder.accountIcon.setImageDrawable(account.getDisplayIcon(mContext));
-
             final ContactPhotoManager.DefaultImageRequest
                     request = new ContactPhotoManager.DefaultImageRequest(
-                    displayName, String.valueOf(rawContactId), /* isCircular = */ true);
-            final Uri photoUri = Uri.withAppendedPath(
-                    ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
-                    RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
+                    displayName, String.valueOf(rawContact.id), /* isCircular = */ true);
 
-            ContactPhotoManager.getInstance(mContext).loadDirectoryPhoto(holder.photo,
-                    photoUri,
+            ContactPhotoManager.getInstance(mContext).loadThumbnail(holder.photo,
+                    rawContact.photoId,
                     /* darkTheme = */ false,
                     /* isCircular = */ true,
                     request);
-        }
-
-        @Override
-        public View newView(Context context, Cursor cursor, ViewGroup parent) {
-            final View view = mInflater.inflate(R.layout.raw_contact_list_item, parent, false);
-            final RawContactViewHolder holder = new RawContactViewHolder();
-            holder.displayName = (TextView) view.findViewById(R.id.display_name);
-            holder.accountName = (TextView) view.findViewById(R.id.account_name);
-            holder.accountIcon = (ImageView) view.findViewById(R.id.account_icon);
-            holder.photo = (ImageView) view.findViewById(R.id.photo);
-            view.setTag(holder);
             return view;
         }
 
-        @Override
-        public long getItemId(int position) {
-            getCursor().moveToPosition(position);
-            return getCursor().getLong(PickRawContactLoader.RAW_CONTACT_ID);
-        }
-
         class RawContactViewHolder {
             TextView displayName;
             TextView accountName;
@@ -141,17 +150,14 @@
         }
     }
 
-    // Cursor holding all raw contact rows for the given Contact.
-    private Cursor mCursor;
-    private CursorAdapter mAdapter;
-    private boolean mIsUserProfile;
+    private ListAdapter mAdapter;
+    private boolean mShouldFinishActivity = true;
 
-    public static PickRawContactDialogFragment getInstance(Cursor cursor, boolean isUserProfile) {
+    public static PickRawContactDialogFragment getInstance(RawContactsMetadata metadata) {
         final PickRawContactDialogFragment fragment = new PickRawContactDialogFragment();
         final Bundle args = new Bundle();
-        args.putBoolean(ARGS_IS_USER_PROFILE, isUserProfile);
+        args.putParcelable(ARGS_RAW_CONTACTS_METADATA, metadata);
         fragment.setArguments(args);
-        fragment.setCursor(cursor);
         return fragment;
     }
 
@@ -161,9 +167,47 @@
             throw new IllegalArgumentException(
                     "Host activity doesn't implement PickRawContactListener");
         }
+        final Bundle args = getArguments();
+        if (args == null) {
+            throw new IllegalArgumentException("Dialog created with no arguments");
+        }
+
+        final RawContactsMetadata metadata = args.getParcelable(ARGS_RAW_CONTACTS_METADATA);
+        if (metadata == null) {
+            throw new IllegalArgumentException("Dialog created with null RawContactsMetadata");
+        }
+
         final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
-        mAdapter = new RawContactAccountListAdapter(getContext(), mCursor);
-        builder.setTitle(R.string.contact_editor_pick_raw_contact_dialog_title);
+        mAdapter = new RawContactAccountListAdapter(getContext(), metadata);
+        if (metadata.showReadOnly) {
+            builder.setTitle(R.string.contact_editor_pick_linked_contact_dialog_title);
+            builder.setPositiveButton(R.string.contact_editor_add_linked_contact,
+                    new DialogInterface.OnClickListener() {
+                        @Override
+                        public void onClick(DialogInterface dialog, int which) {
+                            mShouldFinishActivity = false;
+                            final Intent intent = new Intent(getActivity(),
+                                    ContactSelectionActivity.class);
+                            intent.setAction(UiIntentActions.PICK_JOIN_CONTACT_ACTION);
+                            intent.putExtra(UiIntentActions.TARGET_CONTACT_ID_EXTRA_KEY,
+                                    metadata.contactId);
+                            getActivity().startActivityForResult(intent, REQUEST_CODE_JOIN);
+                        }
+                    });
+            builder.setNeutralButton(R.string.contact_editor_unlink_contacts,
+                    new DialogInterface.OnClickListener() {
+                        @Override
+                        public void onClick(DialogInterface dialog, int which) {
+                            mShouldFinishActivity = false;
+                            final SplitContactConfirmationDialogFragment splitDialog = new
+                                    SplitContactConfirmationDialogFragment();
+                            splitDialog.show(getActivity().getFragmentManager(),
+                                    SplitContactConfirmationDialogFragment.TAG);
+                        }
+                    });
+        } else {
+            builder.setTitle(R.string.contact_editor_pick_raw_contact_to_edit_dialog_title);
+        }
         builder.setAdapter(mAdapter, new DialogInterface.OnClickListener() {
             @Override
             public void onClick(DialogInterface dialog, int which) {
@@ -172,23 +216,18 @@
             }
         });
         builder.setCancelable(true);
+        if (savedInstanceState == null) {
+            Logger.logEditorEvent(EditorEvent.EventType.SHOW_RAW_CONTACT_PICKER,
+                    /* numberRawContacts */ mAdapter.getCount());
+        }
         return builder.create();
     }
 
     @Override
     public void onDismiss(DialogInterface dialog) {
         super.onDismiss(dialog);
-        mCursor = null;
-        finishActivity();
-    }
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        final Bundle args = getArguments();
-        if (args != null) {
-            mIsUserProfile = args.getBoolean(ARGS_IS_USER_PROFILE);
+        if (mShouldFinishActivity) {
+            finishActivity();
         }
     }
 
@@ -197,13 +236,6 @@
         return getActivity();
     }
 
-    public void setCursor(Cursor cursor) {
-        if (mAdapter != null) {
-            mAdapter.swapCursor(cursor);
-        }
-        mCursor = cursor;
-    }
-
     private void finishActivity() {
         if (getActivity() != null && !getActivity().isFinishing()) {
             getActivity().finish();
diff --git a/src/com/android/contacts/editor/PickRawContactLoader.java b/src/com/android/contacts/editor/PickRawContactLoader.java
index ca44e21..08bd02f 100644
--- a/src/com/android/contacts/editor/PickRawContactLoader.java
+++ b/src/com/android/contacts/editor/PickRawContactLoader.java
@@ -1,22 +1,35 @@
 package com.android.contacts.editor;
 
+import android.content.AsyncTaskLoader;
+import android.content.ContentResolver;
 import android.content.Context;
-import android.content.CursorLoader;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Profile;
 import android.provider.ContactsContract.RawContacts;
 
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountType;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
 /**
  * Loader for the pick a raw contact to edit activity. Loads all raw contact metadata for the
  * given Contact {@link Uri}.
  */
-public class PickRawContactLoader extends CursorLoader {
+public class PickRawContactLoader extends
+        AsyncTaskLoader<PickRawContactLoader.RawContactsMetadata> {
     private Uri mContactUri;
-    private boolean mIsUserProfile;
+    private RawContactsMetadata mCachedResult;
 
-    public static final String[] COLUMNS = new String[] {
+    private static final String[] RAW_CONTACT_PROJECTION = new String[] {
             RawContacts.ACCOUNT_NAME,
             RawContacts.ACCOUNT_TYPE,
             RawContacts.DATA_SET,
@@ -25,53 +38,132 @@
             RawContacts.DISPLAY_NAME_ALTERNATIVE
     };
 
-    public static final String SELECTION = RawContacts.CONTACT_ID + "=?";
+    private static final String RAW_CONTACT_SELECTION = RawContacts.CONTACT_ID + "=?";
 
-    public static final int ACCOUNT_NAME = 0;
-    public static final int ACCOUNT_TYPE = 1;
-    public static final int DATA_SET = 2;
-    public static final int RAW_CONTACT_ID = 3;
-    public static final int DISPLAY_NAME_PRIMARY = 4;
-    public static final int DISPLAY_NAME_ALTERNATIVE = 5;
+    private static final int ACCOUNT_NAME = 0;
+    private static final int ACCOUNT_TYPE = 1;
+    private static final int DATA_SET = 2;
+    private static final int RAW_CONTACT_ID = 3;
+    private static final int DISPLAY_NAME_PRIMARY = 4;
+    private static final int DISPLAY_NAME_ALTERNATIVE = 5;
+
+    private static final String PHOTO_SELECTION_PREFIX =
+            ContactsContract.Data.RAW_CONTACT_ID + " IN (";
+    private static final String PHOTO_SELECTION_SUFFIX = ") AND " + ContactsContract.Data.MIMETYPE
+            + "=\"" + ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE + "\"";
 
     public PickRawContactLoader(Context context, Uri contactUri) {
-        super(context, ensureIsContactUri(contactUri), COLUMNS, SELECTION, null, RawContacts._ID);
-        mContactUri = contactUri;
+        super(context);
+        mContactUri = ensureIsContactUri(contactUri);
     }
 
     @Override
-    public Cursor loadInBackground() {
+    public RawContactsMetadata loadInBackground() {
+        final ContentResolver resolver = getContext().getContentResolver();
         // Get the id of the contact we're looking at.
-        final Cursor cursor = getContext().getContentResolver()
-                .query(mContactUri, new String[] { Contacts._ID, Contacts.IS_USER_PROFILE }, null,
+        final Cursor contactCursor = resolver.query(
+                mContactUri, new String[] {Contacts._ID, Contacts.IS_USER_PROFILE}, null,
                 null, null);
 
-        if (cursor == null) {
+        if (contactCursor == null) {
             return null;
         }
 
-        if (cursor.getCount() < 1) {
-            cursor.close();
+        if (contactCursor.getCount() < 1) {
+            contactCursor.close();
             return null;
         }
 
-        cursor.moveToFirst();
-        final long contactId = cursor.getLong(/* Contacts._ID */ 0);
-        mIsUserProfile = cursor.getInt(/* Contacts.IS_USER_PROFILE */ 1) == 1;
+        final RawContactsMetadata result = new RawContactsMetadata();
+        try {
+            contactCursor.moveToFirst();
+            result.contactId = contactCursor.getLong(/* Contacts._ID */ 0);
+            result.isUserProfile = contactCursor.getInt(/* Contacts.IS_USER_PROFILE */ 1) == 1;
+        } finally {
+            contactCursor.close();
+        }
 
-        cursor.close();
-        // Update selection arguments and uri.
-        setSelectionArgs(new String[]{ Long.toString(contactId) });
-        if (mIsUserProfile) {
-            setUri(ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI);
+        // Load RawContact data
+        final Uri rawContactUri;
+        if (result.isUserProfile) {
+            rawContactUri = ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI;
         } else {
-            setUri(RawContacts.CONTENT_URI);
+            rawContactUri = RawContacts.CONTENT_URI;
         }
-        return super.loadInBackground();
+
+        final Cursor rawContactCursor = resolver.query(
+                rawContactUri, RAW_CONTACT_PROJECTION, RAW_CONTACT_SELECTION,
+                new String[] {Long.toString(result.contactId)}, null);
+
+        if (rawContactCursor == null) {
+            return null;
+        }
+
+        rawContactCursor.moveToPosition(-1);
+        final StringBuilder photoSelection = new StringBuilder(PHOTO_SELECTION_PREFIX);
+        final Map<Long, RawContact> rawContactMap = new HashMap<>();
+        try {
+            while (rawContactCursor.moveToNext()) {
+                RawContact rawContact = new RawContact();
+                rawContact.id = rawContactCursor.getLong(RAW_CONTACT_ID);
+                photoSelection.append(rawContact.id).append(',');
+                rawContact.displayName = rawContactCursor.getString(DISPLAY_NAME_PRIMARY);
+                rawContact.displayNameAlt = rawContactCursor.getString(DISPLAY_NAME_ALTERNATIVE);
+                rawContact.accountName = rawContactCursor.getString(ACCOUNT_NAME);
+                rawContact.accountType = rawContactCursor.getString(ACCOUNT_TYPE);
+                rawContact.accountDataSet = rawContactCursor.getString(DATA_SET);
+                result.rawContacts.add(rawContact);
+                rawContactMap.put(rawContact.id, rawContact);
+            }
+        } finally {
+            rawContactCursor.close();
+        }
+
+        // Remove the last ','
+        if (photoSelection.length() > 0) {
+            photoSelection.deleteCharAt(photoSelection.length() - 1);
+        }
+        photoSelection.append(PHOTO_SELECTION_SUFFIX);
+
+        final Uri dataUri = result.isUserProfile
+                ? Uri.withAppendedPath(Profile.CONTENT_URI, Data.CONTENT_URI.getPath())
+                : Data.CONTENT_URI;
+        final Cursor photoCursor = resolver.query(
+                dataUri,
+                new String[] {Data.RAW_CONTACT_ID, Contacts.Photo._ID},
+                photoSelection.toString(), null, null);
+
+        if (photoCursor != null) {
+            try {
+                photoCursor.moveToPosition(-1);
+                while (photoCursor.moveToNext()) {
+                    final long rawContactId = photoCursor.getLong(/* Data.RAW_CONTACT_ID */ 0);
+                    rawContactMap.get(rawContactId).photoId =
+                            photoCursor.getLong(/* PHOTO._ID */ 1);
+                }
+            } finally {
+                photoCursor.close();
+            }
+        }
+        return result;
     }
 
-    public boolean isUserProfile() {
-        return mIsUserProfile;
+    @Override
+    public void deliverResult(RawContactsMetadata data) {
+        mCachedResult = data;
+        if (isStarted()) {
+            super.deliverResult(data);
+        }
+    }
+
+    @Override
+    protected void onStartLoading() {
+        super.onStartLoading();
+        if (mCachedResult == null) {
+            forceLoad();
+        } else {
+            deliverResult(mCachedResult);
+        }
     }
 
     /**
@@ -87,4 +179,127 @@
         }
         return uri;
     }
+
+    public static class RawContactsMetadata implements Parcelable {
+        public static final Parcelable.Creator<RawContactsMetadata> CREATOR =
+                new Parcelable.Creator<RawContactsMetadata>() {
+                    @Override
+                    public RawContactsMetadata createFromParcel(Parcel source) {
+                        return new RawContactsMetadata(source);
+                    }
+
+                    @Override
+                    public RawContactsMetadata[] newArray(int size) {
+                        return new RawContactsMetadata[size];
+                    }
+                };
+
+        public long contactId;
+        public boolean isUserProfile;
+        public boolean showReadOnly = false;
+        public ArrayList<RawContact> rawContacts = new ArrayList<>();
+
+        public RawContactsMetadata() {}
+
+        private RawContactsMetadata(Parcel in) {
+            contactId = in.readLong();
+            isUserProfile = in.readInt() == 1;
+            showReadOnly = in.readInt() == 1;
+            in.readTypedList(rawContacts, RawContact.CREATOR);
+        }
+
+        /**
+         * Removes all read-only raw contacts.
+         */
+        public void trimReadOnly(AccountTypeManager accountManager) {
+            for (int i = rawContacts.size() - 1; i >= 0 ; i--) {
+                final RawContact rawContact = rawContacts.get(i);
+                final AccountType account = accountManager.getAccountType(
+                        rawContact.accountType, rawContact.accountDataSet);
+                if (!account.areContactsWritable()) {
+                    rawContacts.remove(i);
+                }
+            }
+        }
+
+        /**
+         * Returns the index of the first writable account in this contact or -1 if none exist.
+         */
+        public int getIndexOfFirstWritableAccount(AccountTypeManager accountManager) {
+            for (int i = 0; i < rawContacts.size(); i++) {
+                final RawContact rawContact = rawContacts.get(i);
+                final AccountType account = accountManager.getAccountType(
+                        rawContact.accountType, rawContact.accountDataSet);
+                if (account.areContactsWritable()) {
+                    return i;
+                }
+            }
+
+            return -1;
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeLong(contactId);
+            dest.writeInt(isUserProfile ? 1 : 0);
+            dest.writeInt(showReadOnly ? 1 : 0);
+            dest.writeTypedList(rawContacts);
+        }
+    }
+
+    public static class RawContact implements Parcelable {
+        public static final Parcelable.Creator<RawContact> CREATOR =
+                new Parcelable.Creator<RawContact>() {
+                    @Override
+                    public RawContact createFromParcel(Parcel source) {
+                        return new RawContact(source);
+                    }
+
+                    @Override
+                    public RawContact[] newArray(int size) {
+                        return new RawContact[size];
+                    }
+                };
+
+        public long id;
+        public long photoId;
+        public String displayName;
+        public String displayNameAlt;
+        public String accountName;
+        public String accountType;
+        public String accountDataSet;
+
+        public RawContact() {}
+
+        private RawContact(Parcel in) {
+            id = in.readLong();
+            photoId = in.readLong();
+            displayName = in.readString();
+            displayNameAlt = in.readString();
+            accountName = in.readString();
+            accountType = in.readString();
+            accountDataSet = in.readString();
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeLong(id);
+            dest.writeLong(photoId);
+            dest.writeString(displayName);
+            dest.writeString(displayNameAlt);
+            dest.writeString(accountName);
+            dest.writeString(accountType);
+            dest.writeString(accountDataSet);
+        }
+    }
 }
diff --git a/src/com/android/contacts/editor/RawContactEditorView.java b/src/com/android/contacts/editor/RawContactEditorView.java
index fe7894a..8df7c37 100644
--- a/src/com/android/contacts/editor/RawContactEditorView.java
+++ b/src/com/android/contacts/editor/RawContactEditorView.java
@@ -212,8 +212,8 @@
 
     // Account header
     private View mAccountHeaderContainer;
-    private TextView mAccountHeaderType;
-    private TextView mAccountHeaderName;
+    private TextView mAccountHeaderPrimaryText;
+    private TextView mAccountHeaderSecondaryText;
     private ImageView mAccountHeaderIcon;
     private ImageView mAccountHeaderExpanderIcon;
 
@@ -254,8 +254,8 @@
 
         // Account header
         mAccountHeaderContainer = findViewById(R.id.account_header_container);
-        mAccountHeaderType = (TextView) findViewById(R.id.account_type);
-        mAccountHeaderName = (TextView) findViewById(R.id.account_name);
+        mAccountHeaderPrimaryText = (TextView) findViewById(R.id.account_type);
+        mAccountHeaderSecondaryText = (TextView) findViewById(R.id.account_name);
         mAccountHeaderIcon = (ImageView) findViewById(R.id.account_type_icon);
         mAccountHeaderExpanderIcon = (ImageView) findViewById(R.id.account_expander_icon);
 
@@ -489,6 +489,7 @@
 
         // Setup the view
         addPhotoView();
+        setAccountInfo();
         if (isReadOnlyRawContact()) {
             // We're want to display the inputs fields for a single read only raw contact
             addReadOnlyRawContactEditorViews();
@@ -499,7 +500,6 @@
     }
 
     private void setupEditorNormally() {
-        addAccountInfo();
         addKindSectionViews();
 
         mMoreFields.setVisibility(hasMoreFields() ? View.VISIBLE : View.GONE);
@@ -600,7 +600,6 @@
 
     private void addReadOnlyRawContactEditorViews() {
         mKindSectionViews.removeAllViews();
-        addAccountInfo();
         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(
                 getContext());
         final AccountType type = mCurrentRawContactDelta.getAccountType(accountTypes);
@@ -713,18 +712,23 @@
         mKindSectionViews.addView(field);
     }
 
-    private void addAccountInfo() {
-        mAccountHeaderContainer.setVisibility(View.GONE);
-
+    private void setAccountInfo() {
         final AccountDisplayInfo account =
                 mAccountDisplayInfoFactory.getAccountDisplayInfoFor(mCurrentRawContactDelta);
 
         // Get the account information for the primary raw contact delta
-        final String accountLabel = mIsUserProfile
-                ? EditorUiUtils.getAccountHeaderLabelForMyProfile(getContext(), account)
-                : account.getNameLabel().toString();
-
-        addAccountHeader(accountLabel);
+        if (isReadOnlyRawContact()) {
+            final String accountType = account.getTypeLabel().toString();
+            setAccountHeader(accountType,
+                    getResources().getString(
+                            R.string.editor_account_selector_read_only_title, accountType));
+        } else {
+            final String accountLabel = mIsUserProfile
+                    ? EditorUiUtils.getAccountHeaderLabelForMyProfile(getContext(), account)
+                    : account.getNameLabel().toString();
+            setAccountHeader(getResources().getString(R.string.editor_account_selector_title),
+                    accountLabel);
+        }
 
         // If we're saving a new contact and there are multiple accounts, add the account selector.
         final List<AccountWithDataSet> accounts =
@@ -734,18 +738,9 @@
         }
     }
 
-    private void addAccountHeader(String accountLabel) {
-        mAccountHeaderContainer.setVisibility(View.VISIBLE);
-
-        // Set the account name
-        mAccountHeaderName.setVisibility(View.VISIBLE);
-        mAccountHeaderName.setText(accountLabel);
-
-        // Set the account type
-        final String selectorTitle = getResources().getString(isReadOnlyRawContact() ?
-                R.string.editor_account_selector_read_only_title :
-                R.string.editor_account_selector_title);
-        mAccountHeaderType.setText(selectorTitle);
+    private void setAccountHeader(String primaryText, String secondaryText) {
+        mAccountHeaderPrimaryText.setText(primaryText);
+        mAccountHeaderSecondaryText.setText(secondaryText);
 
         // Set the icon
         final AccountType accountType =
@@ -754,8 +749,8 @@
 
         // Set the content description
         mAccountHeaderContainer.setContentDescription(
-                EditorUiUtils.getAccountInfoContentDescription(accountLabel,
-                        selectorTitle));
+                EditorUiUtils.getAccountInfoContentDescription(primaryText,
+                        secondaryText));
     }
 
     private void addAccountSelector(final RawContactDelta rawContactDelta) {
diff --git a/src/com/android/contacts/editor/SelectAccountDialogFragment.java b/src/com/android/contacts/editor/SelectAccountDialogFragment.java
index 34fac4f..4335239 100644
--- a/src/com/android/contacts/editor/SelectAccountDialogFragment.java
+++ b/src/com/android/contacts/editor/SelectAccountDialogFragment.java
@@ -20,7 +20,6 @@
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.app.DialogFragment;
-import android.app.Fragment;
 import android.app.FragmentManager;
 import android.content.DialogInterface;
 import android.os.Bundle;
@@ -48,22 +47,18 @@
      * Show the dialog.
      *
      * @param fragmentManager {@link FragmentManager}.
-     * @param targetFragment {@link Fragment} that implements {@link Listener}.
      * @param titleResourceId resource ID to use as the title.
      * @param accountListFilter account filter.
      * @param extraArgs Extra arguments, which will later be passed to
      *     {@link Listener#onAccountChosen}.  {@code null} will be converted to
      *     {@link Bundle#EMPTY}.
      */
-    public static <F extends Fragment & Listener> void show(FragmentManager fragmentManager,
-            F targetFragment, int titleResourceId,
+    public static void show(FragmentManager fragmentManager, int titleResourceId,
             AccountListFilter accountListFilter, Bundle extraArgs) {
-        show(fragmentManager, targetFragment, titleResourceId, accountListFilter, extraArgs,
-                /* tag */ null);
+        show(fragmentManager, titleResourceId, accountListFilter, extraArgs, /* tag */ null);
     }
 
-    public static <F extends Fragment & Listener> void show(FragmentManager fragmentManager,
-            F targetFragment, int titleResourceId,
+    public static void show(FragmentManager fragmentManager, int titleResourceId,
             AccountListFilter accountListFilter, Bundle extraArgs, String tag) {
         final Bundle args = new Bundle();
         args.putInt(KEY_TITLE_RES_ID, titleResourceId);
@@ -72,9 +67,6 @@
 
         final SelectAccountDialogFragment instance = new SelectAccountDialogFragment();
         instance.setArguments(args);
-        if (targetFragment != null) {
-            instance.setTargetFragment(targetFragment, 0);
-        }
         instance.show(fragmentManager, tag);
     }
 
@@ -115,14 +107,8 @@
         }
     }
 
-    @Override
-    public void onSaveInstanceState(Bundle b) {
-        setTargetFragment(null, -1);
-        super.onSaveInstanceState(b);
-    }
-
     /**
-     * Calls {@link Listener#onAccountChosen} of {@code targetFragment}.
+     * Calls {@link Listener#onAccountChosen}.
      */
     private void onAccountSelected(AccountWithDataSet account) {
         final Listener listener = getListener();
@@ -133,14 +119,9 @@
 
     private Listener getListener() {
         Listener listener = null;
-        final Fragment targetFragment = getTargetFragment();
-        if (targetFragment == null) {
-            final Activity activity = getActivity();
-            if (activity != null && activity instanceof Listener) {
-                listener = (Listener) activity;
-            }
-        } else if (targetFragment instanceof Listener) {
-            listener = (Listener) targetFragment;
+        final Activity activity = getActivity();
+        if (activity != null && activity instanceof Listener) {
+            listener = (Listener) activity;
         }
         return listener;
     }
diff --git a/src/com/android/contacts/editor/SplitContactConfirmationDialogFragment.java b/src/com/android/contacts/editor/SplitContactConfirmationDialogFragment.java
index 0c04466..950b8c6 100644
--- a/src/com/android/contacts/editor/SplitContactConfirmationDialogFragment.java
+++ b/src/com/android/contacts/editor/SplitContactConfirmationDialogFragment.java
@@ -34,6 +34,7 @@
 public class SplitContactConfirmationDialogFragment extends DialogFragment {
 
     private static final String ARG_HAS_PENDING_CHANGES = "hasPendingChanges";
+    public static final String TAG = "splitContactConfirmation";
 
     /**
      * Callbacks for the dialog host.
@@ -47,6 +48,11 @@
          *         that should be saved before the split.
          */
         void onSplitContactConfirmed(boolean hasPendingChanges);
+
+        /**
+         * Invoked if the user has canceled or dismissed the dialog without making a choice.
+         */
+        void onSplitContactCanceled();
     }
 
     public static void show(ContactEditorFragment fragment, boolean hasPendingChanges) {
@@ -65,8 +71,8 @@
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        mHasPendingChanges = getArguments() == null
-                ? false : getArguments().getBoolean(ARG_HAS_PENDING_CHANGES);
+        mHasPendingChanges = getArguments() != null
+                && getArguments().getBoolean(ARG_HAS_PENDING_CHANGES);
     }
 
     @Override
@@ -81,14 +87,28 @@
                 new DialogInterface.OnClickListener() {
                     @Override
                     public void onClick(DialogInterface dialog, int which) {
-                        final Listener targetListener = getTargetFragment() == null
-                                ? (Listener) getActivity()
-                                : (Listener) getTargetFragment();
-                        targetListener.onSplitContactConfirmed(mHasPendingChanges);
+                        getListener().onSplitContactConfirmed(mHasPendingChanges);
                     }
                 });
-        builder.setNegativeButton(android.R.string.cancel, null);
+        builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                onCancel(dialog);
+            }
+        });
         builder.setCancelable(false);
         return builder.create();
     }
+
+    private Listener getListener() {
+        return getTargetFragment() == null
+                ? (Listener) getActivity()
+                : (Listener) getTargetFragment();
+    }
+
+    @Override
+    public void onCancel(DialogInterface dialog) {
+        super.onCancel(dialog);
+        getListener().onSplitContactCanceled();
+    }
 }
diff --git a/src/com/android/contacts/group/GroupMembersFragment.java b/src/com/android/contacts/group/GroupMembersFragment.java
index e71b3a4..44bda74 100644
--- a/src/com/android/contacts/group/GroupMembersFragment.java
+++ b/src/com/android/contacts/group/GroupMembersFragment.java
@@ -50,7 +50,6 @@
 import com.android.contacts.R;
 import com.android.contacts.activities.ActionBarAdapter;
 import com.android.contacts.common.ContactsUtils;
-import com.android.contacts.common.Experiments;
 import com.android.contacts.common.list.ContactsSectionIndexer;
 import com.android.contacts.common.list.MultiSelectEntryContactListAdapter.DeleteContactListener;
 import com.android.contacts.common.logging.ListEvent;
@@ -65,7 +64,6 @@
 import com.android.contacts.list.MultiSelectContactsListFragment;
 import com.android.contacts.list.UiIntentActions;
 import com.android.contactsbind.FeedbackHelper;
-import com.android.contactsbind.experiments.Flags;
 import com.google.common.primitives.Longs;
 
 import java.util.ArrayList;
@@ -91,7 +89,6 @@
     private static final int LOADER_GROUP_METADATA = 0;
     private static final int MSG_FAIL_TO_LOAD = 1;
     private static final int RESULT_GROUP_ADD_MEMBER = 100;
-    private static final int RESULT_SEND_TO_SELECTION = 200;
 
     /** Filters out duplicate contacts. */
     private class FilterCursorWrapper extends CursorWrapper {
@@ -268,13 +265,9 @@
         final boolean isSelectionMode = mActionBarAdapter.isSelectionMode();
         final boolean isGroupEditable = mGroupMetaData != null && mGroupMetaData.editable;
         final boolean isGroupReadOnly = mGroupMetaData != null && mGroupMetaData.readOnly;
-        final boolean experimentFlagSet =
-                Flags.getInstance().getBoolean(Experiments.SEND_TO_GROUP);
 
-        setVisible(menu, R.id.menu_multi_send_email, !mIsEditMode && !isGroupEmpty()
-                && experimentFlagSet);
-        setVisible(menu, R.id.menu_multi_send_message, !mIsEditMode && !isGroupEmpty()
-                && experimentFlagSet);
+        setVisible(menu, R.id.menu_multi_send_email, !mIsEditMode && !isGroupEmpty());
+        setVisible(menu, R.id.menu_multi_send_message, !mIsEditMode && !isGroupEmpty());
         setVisible(menu, R.id.menu_add, isGroupEditable && !isSelectionMode);
         setVisible(menu, R.id.menu_rename_group, !isGroupReadOnly && !isSelectionMode);
         setVisible(menu, R.id.menu_delete_group, !isGroupReadOnly && !isSelectionMode);
@@ -298,7 +291,7 @@
     /**
      * Helper class for cp2 query used to look up all contact's emails and phone numbers.
      */
-    private static abstract class Query {
+    public static abstract class Query {
         public static final String EMAIL_SELECTION =
                 ContactsContract.Data.MIMETYPE + "='"
                         + ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE + "'";
@@ -362,41 +355,6 @@
         }
     }
 
-    private List<String> getSendToDataForIds(long[] ids, String scheme) {
-        final List<String> items = new ArrayList<>();
-        final String sIds = GroupUtil.convertArrayToString(ids);
-        final String select = (ContactsUtils.SCHEME_MAILTO.equals(scheme)
-                ? Query.EMAIL_SELECTION
-                + " AND " + ContactsContract.CommonDataKinds.Email._ID + " IN (" + sIds + ")"
-                : Query.PHONE_SELECTION
-                + " AND " + ContactsContract.CommonDataKinds.Phone._ID + " IN (" + sIds + ")");
-        final ContentResolver contentResolver = getContext().getContentResolver();
-        final Cursor cursor = contentResolver.query(ContactsContract.Data.CONTENT_URI,
-                ContactsUtils.SCHEME_MAILTO.equals(scheme)
-                    ? Query.EMAIL_PROJECTION
-                    : Query.PHONE_PROJECTION,
-                select, null, null);
-
-        if (cursor == null) {
-            return items;
-        }
-
-        try {
-            cursor.moveToPosition(-1);
-            while (cursor.moveToNext()) {
-                final String data = cursor.getString(Query.DATA1);
-
-                if (!TextUtils.isEmpty(data)) {
-                    items.add(data);
-                }
-            }
-        } finally {
-            cursor.close();
-        }
-
-        return items;
-    }
-
     private void sendToGroup(long[] ids, String sendScheme, String title) {
         if (ids == null || ids.length == 0) return;
 
@@ -475,17 +433,13 @@
         }
 
         final String itemsString = TextUtils.join(",", itemList);
-        startSendToSelectionActivity(itemsString, sendScheme, title);
-    }
-
-    private void startSendToSelectionActivity(String listItems, String sendScheme, String title) {
-        startActivity(GroupUtil.createSendToSelectionIntent(listItems, sendScheme, title));
+        GroupUtil.startSendToSelectionActivity(this, itemsString, sendScheme, title);
     }
 
     private void startSendToSelectionPickerActivity(long[] ids, long[] defaultSelection,
             String sendScheme, String title) {
-        startActivityForResult(GroupUtil.createSendToSelectionPickerIntent(getContext(), ids,
-                defaultSelection, sendScheme, title), RESULT_SEND_TO_SELECTION);
+        startActivity(GroupUtil.createSendToSelectionPickerIntent(getContext(), ids,
+                defaultSelection, sendScheme, title));
     }
 
     private void startGroupAddMemberActivity() {
@@ -559,38 +513,25 @@
 
     @Override
     public void onActivityResult(int requestCode, int resultCode, Intent data) {
-        if (resultCode != Activity.RESULT_OK || data == null) {
+        if (resultCode != Activity.RESULT_OK || data == null
+                || requestCode != RESULT_GROUP_ADD_MEMBER) {
             return;
         }
-        switch(requestCode) {
-            case RESULT_GROUP_ADD_MEMBER: {
-                long[] contactIds = data.getLongArrayExtra(
-                        UiIntentActions.TARGET_CONTACT_IDS_EXTRA_KEY);
-                if (contactIds == null) {
-                    final long contactId = data.getLongExtra(
-                            UiIntentActions.TARGET_CONTACT_ID_EXTRA_KEY, -1);
-                    if (contactId > -1) {
-                        contactIds = new long[1];
-                        contactIds[0] = contactId;
-                    }
-                }
-                new UpdateGroupMembersAsyncTask(
-                        UpdateGroupMembersAsyncTask.TYPE_ADD,
-                        getContext(), contactIds, mGroupMetaData.groupId, mGroupMetaData.accountName,
-                        mGroupMetaData.accountType, mGroupMetaData.dataSet).execute();
-                break;
-            }
-            case RESULT_SEND_TO_SELECTION: {
-                final long[] ids = data.getLongArrayExtra(
-                        UiIntentActions.TARGET_CONTACT_IDS_EXTRA_KEY);
-                final String sendScheme = data.getStringExtra(UiIntentActions.SELECTION_SEND_SCHEME);
-                final String sendTitle = data.getStringExtra(UiIntentActions.SELECTION_SEND_TITLE);
-                final List<String> items = getSendToDataForIds(ids, sendScheme);
-                final String list = TextUtils.join(",", items);
-                startSendToSelectionActivity(list, sendScheme, sendTitle);
-                break;
+
+        long[] contactIds = data.getLongArrayExtra(
+                UiIntentActions.TARGET_CONTACT_IDS_EXTRA_KEY);
+        if (contactIds == null) {
+            final long contactId = data.getLongExtra(
+                    UiIntentActions.TARGET_CONTACT_ID_EXTRA_KEY, -1);
+            if (contactId > -1) {
+                contactIds = new long[1];
+                contactIds[0] = contactId;
             }
         }
+        new UpdateGroupMembersAsyncTask(
+                UpdateGroupMembersAsyncTask.TYPE_ADD,
+                getContext(), contactIds, mGroupMetaData.groupId, mGroupMetaData.accountName,
+                mGroupMetaData.accountType, mGroupMetaData.dataSet).execute();
     }
 
     private final ActionBarAdapter.Listener mActionBarListener = new ActionBarAdapter.Listener() {
diff --git a/src/com/android/contacts/group/GroupUtil.java b/src/com/android/contacts/group/GroupUtil.java
index dc85152..3b79546 100644
--- a/src/com/android/contacts/group/GroupUtil.java
+++ b/src/com/android/contacts/group/GroupUtil.java
@@ -16,6 +16,8 @@
 
 package com.android.contacts.group;
 
+import android.app.Fragment;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.database.Cursor;
@@ -58,6 +60,8 @@
     public static final String ACTION_SWITCH_GROUP = "switchGroup";
     public static final String ACTION_UPDATE_GROUP = "updateGroup";
 
+    public static final int RESULT_SEND_TO_SELECTION = 100;
+
     // System IDs of FFC groups in Google accounts
     private static final Set<String> FFC_GROUPS =
             new HashSet(Arrays.asList("Friends", "Family", "Coworkers"));
@@ -100,12 +104,48 @@
                 isFirstGroupInAccount, memberCount, isReadOnly, systemId);
     }
 
+    public static List<String> getSendToDataForIds(Context context, long[] ids, String scheme) {
+        final List<String> items = new ArrayList<>();
+        final String sIds = GroupUtil.convertArrayToString(ids);
+        final String select = (ContactsUtils.SCHEME_MAILTO.equals(scheme)
+                ? GroupMembersFragment.Query.EMAIL_SELECTION
+                + " AND " + ContactsContract.CommonDataKinds.Email._ID + " IN (" + sIds + ")"
+                : GroupMembersFragment.Query.PHONE_SELECTION
+                + " AND " + ContactsContract.CommonDataKinds.Phone._ID + " IN (" + sIds + ")");
+        final ContentResolver contentResolver = context.getContentResolver();
+        final Cursor cursor = contentResolver.query(ContactsContract.Data.CONTENT_URI,
+                ContactsUtils.SCHEME_MAILTO.equals(scheme)
+                        ? GroupMembersFragment.Query.EMAIL_PROJECTION
+                        : GroupMembersFragment.Query.PHONE_PROJECTION,
+                select, null, null);
+
+        if (cursor == null) {
+            return items;
+        }
+
+        try {
+            cursor.moveToPosition(-1);
+            while (cursor.moveToNext()) {
+                final String data = cursor.getString(GroupMembersFragment.Query.DATA1);
+
+                if (!TextUtils.isEmpty(data)) {
+                    items.add(data);
+                }
+            }
+        } finally {
+            cursor.close();
+        }
+
+        return items;
+    }
+
     /** Returns an Intent to send emails/phones to some activity/app */
-    public static Intent createSendToSelectionIntent(
-            String itemsList, String sendScheme, String title) {
+    public static void startSendToSelectionActivity(
+            Fragment fragment, String itemsList, String sendScheme, String title) {
         final Intent intent = new Intent(Intent.ACTION_SENDTO,
                 Uri.fromParts(sendScheme, itemsList, null));
-        return Intent.createChooser(intent, title);
+        fragment.startActivityForResult(
+                Intent.createChooser(intent, title), RESULT_SEND_TO_SELECTION);
     }
 
     /** Returns an Intent to pick emails/phones to send to selection (or group) */
diff --git a/src/com/android/contacts/interactions/CallLogInteraction.java b/src/com/android/contacts/interactions/CallLogInteraction.java
index 9e6b5a2..e207b29 100644
--- a/src/com/android/contacts/interactions/CallLogInteraction.java
+++ b/src/com/android/contacts/interactions/CallLogInteraction.java
@@ -131,12 +131,12 @@
         switch (type) {
             case Calls.INCOMING_TYPE:
                 callArrow = res.getDrawable(CALL_ARROW_ICON_RES);
-                callArrow.setColorFilter(res.getColor(R.color.call_arrow_green),
+                callArrow.mutate().setColorFilter(res.getColor(R.color.call_arrow_green),
                         PorterDuff.Mode.MULTIPLY);
                 break;
             case Calls.MISSED_TYPE:
                 callArrow = res.getDrawable(CALL_ARROW_ICON_RES);
-                callArrow.setColorFilter(res.getColor(R.color.call_arrow_red),
+                callArrow.mutate().setColorFilter(res.getColor(R.color.call_arrow_red),
                         PorterDuff.Mode.MULTIPLY);
                 break;
             case Calls.OUTGOING_TYPE:
diff --git a/src/com/android/contacts/list/DefaultContactBrowseListFragment.java b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
index 331c566..f7ae30d 100644
--- a/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
+++ b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
@@ -190,9 +190,7 @@
                     setSyncOffAlert();
 
                     // Determine whether the account has pullToRefresh feature
-                    if (Flags.getInstance().getBoolean(Experiments.PULL_TO_REFRESH)) {
-                        setSwipeRefreshLayoutEnabledOrNot(getFilter());
-                    }
+                    setSwipeRefreshLayoutEnabledOrNot(getFilter());
                     break;
                 case ActionBarAdapter.Listener.Action.CHANGE_SEARCH_QUERY:
                     final String queryString = mActionBarAdapter.getQueryString();
@@ -464,9 +462,8 @@
     protected void onCreateView(LayoutInflater inflater, ViewGroup container) {
         super.onCreateView(inflater, container);
 
-        if (Flags.getInstance().getBoolean(Experiments.PULL_TO_REFRESH)) {
-            initSwipeRefreshLayout();
-        }
+        initSwipeRefreshLayout();
+
         // Putting the header view inside a container will allow us to make
         // it invisible later. See checkHeaderViewVisibility()
         final FrameLayout headerContainer = new FrameLayout(inflater.getContext());
@@ -720,7 +717,7 @@
     }
 
     private void setDirectorySearchMode() {
-        if (mContactsRequest.isDirectorySearchEnabled()) {
+        if (mContactsRequest != null && mContactsRequest.isDirectorySearchEnabled()) {
             setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_DEFAULT);
         } else {
             setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_NONE);
@@ -834,9 +831,7 @@
         setSyncOffAlert();
 
         // Determine whether the account has pullToRefresh feature
-        if (Flags.getInstance().getBoolean(Experiments.PULL_TO_REFRESH)) {
-            setSwipeRefreshLayoutEnabledOrNot(filter);
-        }
+        setSwipeRefreshLayoutEnabledOrNot(filter);
     }
 
     private void setSwipeRefreshLayoutEnabledOrNot(ContactListFilter filter) {
diff --git a/src/com/android/contacts/list/JoinContactListFragment.java b/src/com/android/contacts/list/JoinContactListFragment.java
index d961f34..d827eb5 100644
--- a/src/com/android/contacts/list/JoinContactListFragment.java
+++ b/src/com/android/contacts/list/JoinContactListFragment.java
@@ -146,7 +146,7 @@
     @Override
     public JoinContactListAdapter createListAdapter() {
         JoinContactListAdapter adapter = new JoinContactListAdapter(getActivity());
-        adapter.setPhotoPosition(ContactListItemView.getDefaultPhotoPosition(true /* opposite */));
+        adapter.setPhotoPosition(ContactListItemView.getDefaultPhotoPosition(false /* opposite */));
         return adapter;
     }
 
diff --git a/src/com/android/contacts/list/MultiSelectContactsListFragment.java b/src/com/android/contacts/list/MultiSelectContactsListFragment.java
index 41fd11e..10e9370 100644
--- a/src/com/android/contacts/list/MultiSelectContactsListFragment.java
+++ b/src/com/android/contacts/list/MultiSelectContactsListFragment.java
@@ -45,6 +45,7 @@
 import com.android.contacts.common.model.account.AccountType;
 import com.android.contacts.common.model.account.AccountWithDataSet;
 import com.android.contacts.common.model.account.GoogleAccountType;
+import com.android.contacts.group.GroupMembersFragment;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -375,10 +376,14 @@
 
         bindListHeaderCommon(listView, accountFilterContainer);
 
-        // Set text of count of contacts and account name (if it's a Google account)
+        final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(context);
+        final AccountType accountType = accountTypeManager.getAccountType(
+                accountWithDataSet.type, accountWithDataSet.dataSet);
+
+        // Set text of count of contacts and account name
         final TextView accountFilterHeader = (TextView) accountFilterContainer.findViewById(
                 R.id.account_filter_header);
-        final String headerText = GoogleAccountType.ACCOUNT_TYPE.equals(accountWithDataSet.type)
+        final String headerText = shouldShowAccountName(accountType)
                 ? String.format(context.getResources().getQuantityString(
                         R.plurals.contacts_count_with_account, memberCount),
                                 memberCount, accountWithDataSet.name)
@@ -388,9 +393,6 @@
         accountFilterHeader.setAllCaps(false);
 
         // Set icon of the account
-        final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(context);
-        final AccountType accountType = accountTypeManager.getAccountType(
-                accountWithDataSet.type, accountWithDataSet.dataSet);
         final Drawable icon = accountType != null ? accountType.getDisplayIcon(context) : null;
         final ImageView accountFilterHeaderIcon = (ImageView) accountFilterContainer
                 .findViewById(R.id.account_filter_icon);
@@ -427,6 +429,11 @@
         accountFilterHeaderIcon.setImageDrawable(icon);
     }
 
+    private boolean shouldShowAccountName(AccountType accountType) {
+        return (accountType.isGroupMembershipEditable() && this instanceof GroupMembersFragment)
+                || GoogleAccountType.ACCOUNT_TYPE.equals(accountType.accountType);
+    }
+
     private void setMargins(View v, int l, int r) {
         if (v.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) {
             ViewGroup.MarginLayoutParams p = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
diff --git a/src/com/android/contacts/list/MultiSelectEmailAddressesListAdapter.java b/src/com/android/contacts/list/MultiSelectEmailAddressesListAdapter.java
index b225ba4..af9ee52 100644
--- a/src/com/android/contacts/list/MultiSelectEmailAddressesListAdapter.java
+++ b/src/com/android/contacts/list/MultiSelectEmailAddressesListAdapter.java
@@ -160,7 +160,6 @@
         cursor.moveToPosition(position);
 
         bindViewId(view, cursor, EmailQuery.EMAIL_ID);
-        bindSectionHeaderAndDivider(view, position);
         if (isFirstEntry) {
             bindName(view, cursor);
             bindQuickContact(view, partition, cursor, EmailQuery.PHOTO_ID,
@@ -190,16 +189,6 @@
         view.showData(cursor, EmailQuery.EMAIL_ADDRESS);
     }
 
-    protected void bindSectionHeaderAndDivider(final ContactListItemView view, int position) {
-        final int section = getSectionForPosition(position);
-        if (getPositionForSection(section) == position) {
-            final String title = (String)getSections()[section];
-            view.setSectionHeader(title);
-        } else {
-            view.setSectionHeader(null);
-        }
-    }
-
     protected void bindName(final ContactListItemView view, Cursor cursor) {
         view.showDisplayName(cursor, EmailQuery.DISPLAY_NAME, getContactNameDisplayOrder());
     }
diff --git a/src/com/android/contacts/list/MultiSelectEmailAddressesListFragment.java b/src/com/android/contacts/list/MultiSelectEmailAddressesListFragment.java
index d3aa5ca..956e473 100644
--- a/src/com/android/contacts/list/MultiSelectEmailAddressesListFragment.java
+++ b/src/com/android/contacts/list/MultiSelectEmailAddressesListFragment.java
@@ -15,9 +15,9 @@
  */
 package com.android.contacts.list;
 
-import android.app.Activity;
 import android.content.Intent;
 import android.os.Bundle;
+import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuInflater;
@@ -27,7 +27,9 @@
 
 import com.android.contacts.R;
 import com.android.contacts.common.logging.ListEvent;
+import com.android.contacts.group.GroupUtil;
 
+import java.util.List;
 import java.util.TreeSet;
 
 /** Displays a list of emails with check boxes. */
@@ -36,7 +38,7 @@
 
     public MultiSelectEmailAddressesListFragment() {
         setPhotoLoaderEnabled(true);
-        setSectionHeaderDisplayEnabled(true);
+        setSectionHeaderDisplayEnabled(false);
         setSearchMode(false);
         setHasOptionsMenu(true);
         setListType(ListEvent.ListType.PICK_EMAIL);
@@ -74,6 +76,11 @@
     }
 
     @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        getActivity().finish();
+    }
+
+    @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         switch(item.getItemId()) {
             case R.id.menu_send: {
@@ -81,13 +88,10 @@
                         UiIntentActions.SELECTION_SEND_SCHEME);
                 final String title= getActivity().getIntent().getStringExtra(
                         UiIntentActions.SELECTION_SEND_TITLE);
-                final Intent intent = new Intent();
-                intent.putExtra(UiIntentActions.TARGET_CONTACT_IDS_EXTRA_KEY,
-                        getAdapter().getSelectedContactIdsArray());
-                intent.putExtra(UiIntentActions.SELECTION_SEND_SCHEME, scheme);
-                intent.putExtra(UiIntentActions.SELECTION_SEND_TITLE, title);
-                getActivity().setResult(Activity.RESULT_OK, intent);
-                getActivity().finish();
+                final List<String> items = GroupUtil.getSendToDataForIds(
+                        getActivity(), getAdapter().getSelectedContactIdsArray(), scheme);
+                final String list = TextUtils.join(",", items);
+                GroupUtil.startSendToSelectionActivity(this, list, scheme, title);
                 return true;
             }
         }
diff --git a/src/com/android/contacts/list/MultiSelectPhoneNumbersListAdapter.java b/src/com/android/contacts/list/MultiSelectPhoneNumbersListAdapter.java
index b3574ab..985809b 100644
--- a/src/com/android/contacts/list/MultiSelectPhoneNumbersListAdapter.java
+++ b/src/com/android/contacts/list/MultiSelectPhoneNumbersListAdapter.java
@@ -160,7 +160,6 @@
         cursor.moveToPosition(position);
 
         bindViewId(view, cursor, PhoneQuery.PHONE_ID);
-        bindSectionHeaderAndDivider(view, position);
         if (isFirstEntry) {
             bindName(view, cursor);
             bindQuickContact(view, partition, cursor, PhoneQuery.PHOTO_ID,
@@ -190,15 +189,6 @@
         view.showData(cursor, PhoneQuery.PHONE_NUMBER);
     }
 
-    protected void bindSectionHeaderAndDivider(final ContactListItemView view, int position) {
-        if (isSectionHeaderDisplayEnabled()) {
-            Placement placement = getItemPlacementInSection(position);
-            view.setSectionHeader(placement.firstInSection ? placement.sectionHeader : null);
-        } else {
-            view.setSectionHeader(null);
-        }
-    }
-
     protected void bindName(final ContactListItemView view, Cursor cursor) {
         view.showDisplayName(cursor, PhoneQuery.DISPLAY_NAME, getContactNameDisplayOrder());
     }
diff --git a/src/com/android/contacts/list/MultiSelectPhoneNumbersListFragment.java b/src/com/android/contacts/list/MultiSelectPhoneNumbersListFragment.java
index aea89e4..751449b 100644
--- a/src/com/android/contacts/list/MultiSelectPhoneNumbersListFragment.java
+++ b/src/com/android/contacts/list/MultiSelectPhoneNumbersListFragment.java
@@ -15,9 +15,9 @@
  */
 package com.android.contacts.list;
 
-import android.app.Activity;
 import android.content.Intent;
 import android.os.Bundle;
+import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuInflater;
@@ -27,7 +27,9 @@
 
 import com.android.contacts.R;
 import com.android.contacts.common.logging.ListEvent;
+import com.android.contacts.group.GroupUtil;
 
+import java.util.List;
 import java.util.TreeSet;
 
 /** Displays a list of phone numbers with check boxes. */
@@ -36,7 +38,7 @@
 
     public MultiSelectPhoneNumbersListFragment() {
         setPhotoLoaderEnabled(true);
-        setSectionHeaderDisplayEnabled(true);
+        setSectionHeaderDisplayEnabled(false);
         setSearchMode(false);
         setHasOptionsMenu(true);
         setListType(ListEvent.ListType.PICK_PHONE);
@@ -74,6 +76,11 @@
     }
 
     @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        getActivity().finish();
+    }
+
+    @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         switch(item.getItemId()) {
             case R.id.menu_send: {
@@ -81,13 +88,10 @@
                         UiIntentActions.SELECTION_SEND_SCHEME);
                 final String title= getActivity().getIntent().getStringExtra(
                         UiIntentActions.SELECTION_SEND_TITLE);
-                final Intent intent = new Intent();
-                intent.putExtra(UiIntentActions.TARGET_CONTACT_IDS_EXTRA_KEY,
-                        getAdapter().getSelectedContactIdsArray());
-                intent.putExtra(UiIntentActions.SELECTION_SEND_SCHEME, scheme);
-                intent.putExtra(UiIntentActions.SELECTION_SEND_TITLE, title);
-                getActivity().setResult(Activity.RESULT_OK, intent);
-                getActivity().finish();
+                final List<String> items = GroupUtil.getSendToDataForIds(
+                        getActivity(), getAdapter().getSelectedContactIdsArray(), scheme);
+                final String list = TextUtils.join(",", items);
+                GroupUtil.startSendToSelectionActivity(this, list, scheme, title);
                 return true;
             }
         }
diff --git a/src/com/android/contacts/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java
index 52c66d5..a98baad 100644
--- a/src/com/android/contacts/quickcontact/QuickContactActivity.java
+++ b/src/com/android/contacts/quickcontact/QuickContactActivity.java
@@ -23,12 +23,15 @@
 import android.animation.ObjectAnimator;
 import android.app.Activity;
 import android.app.LoaderManager.LoaderCallbacks;
+import android.app.ProgressDialog;
 import android.app.SearchManager;
 import android.content.ActivityNotFoundException;
+import android.content.BroadcastReceiver;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.Loader;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
@@ -70,6 +73,7 @@
 import android.provider.ContactsContract.RawContacts;
 import android.support.graphics.drawable.VectorDrawableCompat;
 import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.LocalBroadcastManager;
 import android.support.v7.graphics.Palette;
 import android.telecom.PhoneAccount;
 import android.telecom.TelecomManager;
@@ -122,7 +126,6 @@
 import com.android.contacts.common.model.Contact;
 import com.android.contacts.common.model.ContactLoader;
 import com.android.contacts.common.model.RawContact;
-import com.android.contacts.common.model.RawContactDeltaList;
 import com.android.contacts.common.model.account.AccountType;
 import com.android.contacts.common.model.dataitem.CustomDataItem;
 import com.android.contacts.common.model.dataitem.DataItem;
@@ -150,7 +153,6 @@
 import com.android.contacts.editor.ContactEditorFragment;
 import com.android.contacts.editor.EditorIntents;
 import com.android.contacts.editor.EditorUiUtils;
-import com.android.contacts.editor.SplitContactConfirmationDialogFragment;
 import com.android.contacts.interactions.CalendarInteractionsLoader;
 import com.android.contacts.interactions.CallLogInteractionsLoader;
 import com.android.contacts.interactions.ContactDeletionInteraction;
@@ -190,8 +192,7 @@
  * data asynchronously, and then shows a popup with details centered around
  * {@link Intent#getSourceBounds()}.
  */
-public class QuickContactActivity extends ContactsActivity implements
-        SplitContactConfirmationDialogFragment.Listener {
+public class QuickContactActivity extends ContactsActivity {
 
     /**
      * QuickContacts immediately takes up the full screen. All possible information is shown.
@@ -275,6 +276,8 @@
     private boolean mHasAlreadyBeenOpened;
     private boolean mOnlyOnePhoneNumber;
     private boolean mOnlyOneEmail;
+    private ProgressDialog mProgressDialog;
+    private SaveServiceListener mListener;
 
     private QuickContactImageView mPhotoView;
     private ExpandingEntryCardView mContactCard;
@@ -729,6 +732,18 @@
                     savedInstanceState.getBoolean(KEY_ARE_PHONE_OPTIONS_CHANGEABLE);
             mCustomRingtone = savedInstanceState.getString(KEY_CUSTOM_RINGTONE);
         }
+        mProgressDialog = new ProgressDialog(this);
+        mProgressDialog.setIndeterminate(true);
+        mProgressDialog.setCancelable(false);
+
+        mListener = new SaveServiceListener();
+        final IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(ContactSaveService.BROADCAST_LINK_COMPLETE);
+        intentFilter.addAction(ContactSaveService.BROADCAST_UNLINK_COMPLETE);
+        LocalBroadcastManager.getInstance(this).registerReceiver(mListener,
+                intentFilter);
+
+
         mShouldLog = true;
 
         // There're 3 states for each permission:
@@ -908,7 +923,7 @@
         } else if (requestCode == REQUEST_CODE_JOIN) {
             // Ignore failed requests
             if (resultCode != Activity.RESULT_OK) {
-                processIntent(data);
+                return;
             }
             if (data != null) {
                 joinAggregate(ContentUris.parseId(data.getData()));
@@ -1243,6 +1258,14 @@
             destroyInteractionLoaders();
             startInteractionLoaders(mCachedCp2DataCardModel);
         }
+        maybeShowProgressDialog();
+    }
+
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        dismissProgressBar();
     }
 
     private void populateContactAndAboutCard(Cp2DataCardModel cp2DataCardModel,
@@ -2450,6 +2473,8 @@
                                 ActionType.UNKNOWN_ACTION, /* thirdPartyAction */ null);
                     }
                     mRecentCard.setVisibility(View.VISIBLE);
+                } else {
+                    mRecentCard.setVisibility(View.GONE);
                 }
 
                 Trace.endSection();
@@ -2688,13 +2713,17 @@
                 editMenuItem.setVisible(false);
             }
 
-            final MenuItem splitMenuItem = menu.findItem(R.id.menu_split);
-            splitMenuItem.setVisible(isContactEditable() && !mContactData.isUserProfile()
-                    && mContactData.isMultipleRawContacts());
-
+            // The link menu item is only visible if this has a single raw contact.
             final MenuItem joinMenuItem = menu.findItem(R.id.menu_join);
             joinMenuItem.setVisible(!InvisibleContactUtil.isInvisibleAndAddable(mContactData, this)
-                    && isContactEditable() && !mContactData.isUserProfile());
+                    && isContactEditable() && !mContactData.isUserProfile()
+                    && !mContactData.isMultipleRawContacts());
+
+            // Viewing linked contacts can only happen if there are multiple raw contacts and
+            // the link menu isn't available.
+            final MenuItem linkedContactsMenuItem = menu.findItem(R.id.menu_linked_contacts);
+            linkedContactsMenuItem.setVisible(mContactData.isMultipleRawContacts()
+                    && !joinMenuItem.isVisible());
 
             final MenuItem deleteMenuItem = menu.findItem(R.id.menu_delete);
             deleteMenuItem.setVisible(isContactEditable() && !mContactData.isUserProfile());
@@ -2809,10 +2838,10 @@
                     editContact();
                 }
                 return true;
-            case R.id.menu_split:
-                return doSplitContactAction();
             case R.id.menu_join:
                 return doJoinContactAction();
+            case R.id.menu_linked_contacts:
+                return showRawContactPickerDialog();
             case R.id.menu_delete:
                 Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD,
                         ActionType.REMOVE, /* thirdPartyAction */ null);
@@ -2859,6 +2888,18 @@
         }
     }
 
+    private boolean showRawContactPickerDialog() {
+        if (mContactData == null) return false;
+        startActivityForResult(EditorIntents.createViewLinkedContactsIntent(
+                QuickContactActivity.this,
+                mContactData.getLookupUri(),
+                mHasComputedThemeColor
+                        ? new MaterialPalette(mColorFilterColor, mStatusBarColor)
+                        : null),
+                REQUEST_CODE_CONTACT_EDITOR_ACTIVITY);
+        return true;
+    }
+
     private boolean doJoinContactAction() {
         if (mContactData == null) return false;
 
@@ -2878,34 +2919,9 @@
                 this, mPreviousContactId, contactId, QuickContactActivity.class,
                 Intent.ACTION_VIEW);
         this.startService(intent);
+        showLinkProgressBar();
     }
 
-    private boolean doSplitContactAction() {
-        if (mContactData == null) return false;
-
-        final SplitContactConfirmationDialogFragment dialog = new
-                SplitContactConfirmationDialogFragment();
-        dialog.show(getFragmentManager(), "splitContact");
-        return true;
-    }
-
-    @Override
-    public void onSplitContactConfirmed(boolean hasPendingChanges) {
-        final RawContactDeltaList rawContactDeltaList= mContactData.createRawContactDeltaList();
-        rawContactDeltaList.markRawContactsForSplitting();
-        final Intent intent = ContactSaveService.createSaveContactIntent(this,
-                rawContactDeltaList,
-                /* saveModeExtraKey */ "",
-                /* saveMode */ 0,
-                mContactData.isUserProfile(),
-                ((Activity) this).getClass(),
-                ACTION_SPLIT_COMPLETED,
-                /* updatedPhotos */ null,
-                /* joinContactIdExtraKey =*/ null,
-                /* joinContactId =*/ null);
-        ContactSaveService.startService(this, intent,
-                ContactEditorActivity.ContactEditor.SaveMode.SPLIT);
-    }
 
     private void doPickRingtone() {
         final Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
@@ -2929,4 +2945,46 @@
             Toast.makeText(this, R.string.missing_app, Toast.LENGTH_SHORT).show();
         }
     }
+
+    private void dismissProgressBar() {
+        if (mProgressDialog != null && mProgressDialog.isShowing()) {
+            mProgressDialog.dismiss();
+        }
+    }
+
+    private void showLinkProgressBar() {
+        mProgressDialog.setMessage(getString(R.string.contacts_linking_progress_bar));
+        mProgressDialog.show();
+    }
+
+    private void showUnlinkProgressBar() {
+        mProgressDialog.setMessage(getString(R.string.contacts_unlinking_progress_bar));
+        mProgressDialog.show();
+    }
+
+    private void maybeShowProgressDialog() {
+        if (ContactSaveService.getState().isActionPending(
+                ContactSaveService.ACTION_SPLIT_CONTACT)) {
+            showUnlinkProgressBar();
+        } else if (ContactSaveService.getState().isActionPending(
+                ContactSaveService.ACTION_JOIN_CONTACTS)) {
+            showLinkProgressBar();
+        }
+    }
+
+    private class SaveServiceListener extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Got broadcast from save service " + intent);
+            }
+            if (ContactSaveService.BROADCAST_LINK_COMPLETE.equals(intent.getAction())
+                    || ContactSaveService.BROADCAST_UNLINK_COMPLETE.equals(intent.getAction())) {
+                dismissProgressBar();
+                if (ContactSaveService.BROADCAST_UNLINK_COMPLETE.equals(intent.getAction())) {
+                    finish();
+                }
+            }
+        }
+    }
 }
diff --git a/tests/src/com/android/contacts/RunMethodInstrumentation.java b/tests/src/com/android/contacts/RunMethodInstrumentation.java
index f7e3970..d7ffee9 100644
--- a/tests/src/com/android/contacts/RunMethodInstrumentation.java
+++ b/tests/src/com/android/contacts/RunMethodInstrumentation.java
@@ -19,6 +19,7 @@
 import android.app.Instrumentation;
 import android.content.Context;
 import android.os.Bundle;
+import android.os.Debug;
 import android.support.test.InstrumentationRegistry;
 import android.util.Log;
 
@@ -62,6 +63,9 @@
         Log.d(TAG, "Running " + className + "." + methodName);
         Log.d(TAG, "args=" + args);
 
+        if (arguments.containsKey("debug") && Boolean.parseBoolean(arguments.getString("debug"))) {
+            Debug.waitForDebugger();
+        }
         start();
     }