Implement linked contact menu option
am: b9065ddb28

Change-Id: Iab1b06335ffb88c562e9baa320a16f5b3e369462
diff --git a/AndroidManifest_common.xml b/AndroidManifest_common.xml
index 8fc97ac..09e9c81 100644
--- a/AndroidManifest_common.xml
+++ b/AndroidManifest_common.xml
@@ -366,7 +366,6 @@
 
          <activity
             android:name=".activities.ContactEditorSpringBoardActivity"
-            android:noHistory="true"
             android:theme="@style/TransparentThemeAppCompat">
 
              <intent-filter>
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/strings.xml b/res/values/strings.xml
index 70b8d9e..16af0de 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -147,6 +147,14 @@
     <!-- 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>
 
@@ -746,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
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index d2a65a8..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,8 +47,6 @@
 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;
@@ -72,6 +72,7 @@
 import com.android.contacts.compat.PinnedPositionsCompat;
 import com.android.contacts.util.ContactPhotoUtils;
 import com.android.contactsbind.FeedbackHelper;
+
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 
@@ -81,8 +82,6 @@
 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.
  */
@@ -133,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";
@@ -159,6 +159,8 @@
 
     public static final String BROADCAST_GROUP_DELETED = "groupDeleted";
     public static final String BROADCAST_SIM_IMPORT_COMPLETE = "simImportComplete";
+    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";
 
@@ -1244,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,
@@ -1256,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) {
@@ -1273,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;
@@ -1288,6 +1305,8 @@
             }
             return;
         }
+        LocalBroadcastManager.getInstance(this)
+                .sendBroadcast(new Intent(BROADCAST_UNLINK_COMPLETE));
         if (receiver != null) {
             receiver.send(CONTACTS_SPLIT, new Bundle());
         } else {
@@ -1301,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;
@@ -1312,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)) {
@@ -1453,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());
@@ -1591,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);
     }
@@ -1710,13 +1733,18 @@
     }
 
     /**
-     * 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());
diff --git a/src/com/android/contacts/activities/ContactEditorSpringBoardActivity.java b/src/com/android/contacts/activities/ContactEditorSpringBoardActivity.java
index 9e39c5b..6394d07 100644
--- a/src/com/android/contacts/activities/ContactEditorSpringBoardActivity.java
+++ b/src/com/android/contacts/activities/ContactEditorSpringBoardActivity.java
@@ -1,5 +1,6 @@
 package com.android.contacts.activities;
 
+import android.app.Activity;
 import android.app.FragmentManager;
 import android.app.FragmentTransaction;
 import android.app.LoaderManager;
@@ -13,6 +14,7 @@
 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;
@@ -25,15 +27,18 @@
 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;
@@ -44,6 +49,7 @@
     private RawContactsMetadata mResult;
     private MaterialPalette mMaterialPalette;
     private boolean mHasWritableAccount;
+    private boolean mShowReadOnly;
     private int mWritableAccountPosition;
 
     /**
@@ -65,13 +71,7 @@
                         return;
                     }
                     mResult = result;
-                    maybeTrimReadOnly();
-                    setHasWritableAccount();
-                    if (mResult.rawContacts.size() > 1 && mHasWritableAccount) {
-                        showDialog();
-                    } else {
-                        loadEditor();
-                    }
+                    onLoad();
                 }
 
                 @Override
@@ -103,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();
@@ -130,15 +131,27 @@
     }
 
     /**
+     * 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() {
-        final boolean showReadOnly = getIntent().getBooleanExtra(EXTRA_SHOW_READ_ONLY, false);
-        mResult.showReadOnly = showReadOnly;
-
-        if (showReadOnly) {
+        mResult.showReadOnly = mShowReadOnly;
+        if (mShowReadOnly) {
             return;
         }
+
         mResult.trimReadOnly(AccountTypeManager.getInstance(this));
     }
 
@@ -147,19 +160,18 @@
      */
     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()) {
+        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(
+        final PickRawContactDialogFragment pick = PickRawContactDialogFragment.getInstance(
                  mResult);
-        ft.add(newFragment, TAG_RAW_CONTACTS_DIALOG);
+        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
@@ -185,6 +197,7 @@
             intent.setClass(this, ContactEditorActivity.class);
         }
         startEditorAndForwardExtras(intent);
+        finish();
     }
 
     /**
@@ -225,4 +238,40 @@
         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/editor/ContactEditorFragment.java b/src/com/android/contacts/editor/ContactEditorFragment.java
index dc2171b..384c163 100644
--- a/src/com/android/contacts/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorFragment.java
@@ -799,6 +799,9 @@
         save(SaveMode.SPLIT);
     }
 
+    @Override
+    public void onSplitContactCanceled() {}
+
     private boolean doSplitContactAction() {
         if (!hasValidState()) return false;
 
diff --git a/src/com/android/contacts/editor/EditorIntents.java b/src/com/android/contacts/editor/EditorIntents.java
index e92173c..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.
      */
diff --git a/src/com/android/contacts/editor/PickRawContactDialogFragment.java b/src/com/android/contacts/editor/PickRawContactDialogFragment.java
index 1136f82..23a2b9b 100644
--- a/src/com/android/contacts/editor/PickRawContactDialogFragment.java
+++ b/src/com/android/contacts/editor/PickRawContactDialogFragment.java
@@ -5,6 +5,7 @@
 import android.app.DialogFragment;
 import android.content.Context;
 import android.content.DialogInterface;
+import android.content.Intent;
 import android.os.Bundle;
 import android.text.TextUtils;
 import android.view.LayoutInflater;
@@ -16,6 +17,7 @@
 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;
@@ -28,6 +30,7 @@
 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}.
@@ -36,6 +39,7 @@
  */
 public class PickRawContactDialogFragment extends DialogFragment {
     private static final String ARGS_RAW_CONTACTS_METADATA = "rawContactsMetadata";
+    private static final int REQUEST_CODE_JOIN = 3;
 
     public interface PickRawContactListener {
         void onPickRawContact(long rawContactId);
@@ -147,6 +151,7 @@
     }
 
     private ListAdapter mAdapter;
+    private boolean mShouldFinishActivity = true;
 
     public static PickRawContactDialogFragment getInstance(RawContactsMetadata metadata) {
         final PickRawContactDialogFragment fragment = new PickRawContactDialogFragment();
@@ -174,7 +179,35 @@
 
         final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
         mAdapter = new RawContactAccountListAdapter(getContext(), metadata);
-        builder.setTitle(R.string.contact_editor_pick_raw_contact_to_edit_dialog_title);
+        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) {
@@ -193,7 +226,9 @@
     @Override
     public void onDismiss(DialogInterface dialog) {
         super.onDismiss(dialog);
-        finishActivity();
+        if (mShouldFinishActivity) {
+            finishActivity();
+        }
     }
 
     @Override
diff --git a/src/com/android/contacts/editor/PickRawContactLoader.java b/src/com/android/contacts/editor/PickRawContactLoader.java
index a4f1e5b..08bd02f 100644
--- a/src/com/android/contacts/editor/PickRawContactLoader.java
+++ b/src/com/android/contacts/editor/PickRawContactLoader.java
@@ -75,10 +75,9 @@
         }
 
         final RawContactsMetadata result = new RawContactsMetadata();
-        final long contactId;
         try {
             contactCursor.moveToFirst();
-            contactId = contactCursor.getLong(/* Contacts._ID */ 0);
+            result.contactId = contactCursor.getLong(/* Contacts._ID */ 0);
             result.isUserProfile = contactCursor.getInt(/* Contacts.IS_USER_PROFILE */ 1) == 1;
         } finally {
             contactCursor.close();
@@ -94,7 +93,7 @@
 
         final Cursor rawContactCursor = resolver.query(
                 rawContactUri, RAW_CONTACT_PROJECTION, RAW_CONTACT_SELECTION,
-                new String[] {Long.toString(contactId)}, null);
+                new String[] {Long.toString(result.contactId)}, null);
 
         if (rawContactCursor == null) {
             return null;
@@ -195,6 +194,7 @@
                     }
                 };
 
+        public long contactId;
         public boolean isUserProfile;
         public boolean showReadOnly = false;
         public ArrayList<RawContact> rawContacts = new ArrayList<>();
@@ -202,6 +202,7 @@
         public RawContactsMetadata() {}
 
         private RawContactsMetadata(Parcel in) {
+            contactId = in.readLong();
             isUserProfile = in.readInt() == 1;
             showReadOnly = in.readInt() == 1;
             in.readTypedList(rawContacts, RawContact.CREATOR);
@@ -244,6 +245,7 @@
 
         @Override
         public void writeToParcel(Parcel dest, int flags) {
+            dest.writeLong(contactId);
             dest.writeInt(isUserProfile ? 1 : 0);
             dest.writeInt(showReadOnly ? 1 : 0);
             dest.writeTypedList(rawContacts);
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/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java
index a00bb2f..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:
@@ -1243,6 +1258,14 @@
             destroyInteractionLoaders();
             startInteractionLoaders(mCachedCp2DataCardModel);
         }
+        maybeShowProgressDialog();
+    }
+
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        dismissProgressBar();
     }
 
     private void populateContactAndAboutCard(Cp2DataCardModel cp2DataCardModel,
@@ -2690,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());
@@ -2811,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);
@@ -2861,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;
 
@@ -2880,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);
@@ -2931,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();
+                }
+            }
+        }
+    }
 }