Don't save editor before showing join suggestions

This requires us to pass the raw contact ID of the
contact to join to both the (new) confirmation
dialog and the contact save service so that we
have it to do the join after the save completes.

Bug 25314004
Bug 21956248

Change-Id: Icdcb2165a0e599dfa3745fe8a919b208d4a48b43
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 6cb95fc..0193843 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -147,6 +147,12 @@
     <!-- Confirmation dialog for unlinking contacts into multiple instances [CHAR LIMIT=NONE] -->
     <string name="splitConfirmation">This contact will be unlinked into multiple contacts.</string>
 
+    <!-- Title of the confirmation dialog for joining contacts when there are unsaved changes. [CHAR LIMIT=40] -->
+    <string name="joinConfirmation_title">Link contact?</string>
+
+    <!-- Confirmation dialog message for joining contacts when there are unsaved changes. [CHAR LIMIT=NONE] -->
+    <string name="joinConfirmation">There are unsaved changes. Do you want to save them before linking?</string>
+
     <!-- Menu item that links an aggregate with another aggregate -->
     <string name="menu_joinAggregate">Link</string>
 
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index fd117a4..12ae150 100755
--- a/src/com/android/contacts/ContactSaveService.java
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -313,7 +313,8 @@
         Bundle bundle = new Bundle();
         bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
         return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
-                callbackActivity, callbackAction, bundle);
+                callbackActivity, callbackAction, bundle,
+                /* joinContactIdExtraKey */ null, /* joinContactId */ null);
     }
 
     /**
@@ -321,12 +322,15 @@
      * using data presented as a set of ContentValues.
      * This variant is used when multiple contacts' photos may be updated, as in the
      * Contact Editor.
+     *
      * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
+     * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
+     * @param joinContactId the raw contact ID to join to the contact after doing the save.
      */
     public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
             String saveModeExtraKey, int saveMode, boolean isProfile,
             Class<? extends Activity> callbackActivity, String callbackAction,
-            Bundle updatedPhotos) {
+            Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
         Intent serviceIntent = new Intent(
                 context, ContactSaveService.class);
         serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
@@ -344,6 +348,9 @@
             // the callback intent.
             Intent callbackIntent = new Intent(context, callbackActivity);
             callbackIntent.putExtra(saveModeExtraKey, saveMode);
+            if (joinContactIdExtraKey != null && joinContactId != null) {
+                callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
+            }
             callbackIntent.setAction(callbackAction);
             serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
         }
diff --git a/src/com/android/contacts/activities/ContactEditorBaseActivity.java b/src/com/android/contacts/activities/ContactEditorBaseActivity.java
index aaa04a4..383ae64 100644
--- a/src/com/android/contacts/activities/ContactEditorBaseActivity.java
+++ b/src/com/android/contacts/activities/ContactEditorBaseActivity.java
@@ -170,7 +170,7 @@
          * Invoked after the contact is saved.
          */
         void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded,
-                Uri contactLookupUri);
+                Uri contactLookupUri, Long joinContactId);
 
         /**
          * Invoked after the contact is joined.
@@ -256,7 +256,8 @@
                     intent.getIntExtra(ContactEditorFragment.SAVE_MODE_EXTRA_KEY,
                             ContactEditor.SaveMode.CLOSE),
                     intent.getBooleanExtra(ContactSaveService.EXTRA_SAVE_SUCCEEDED, false),
-                    intent.getData());
+                    intent.getData(),
+                    intent.getLongExtra(ContactEditorFragment.JOIN_CONTACT_ID_EXTRA_KEY, -1));
         } else if (ACTION_JOIN_COMPLETED.equals(action)) {
             mFragment.onJoinCompleted(intent.getData());
         }
diff --git a/src/com/android/contacts/editor/CompactContactEditorFragment.java b/src/com/android/contacts/editor/CompactContactEditorFragment.java
index 54ed0f2..8b4e260 100644
--- a/src/com/android/contacts/editor/CompactContactEditorFragment.java
+++ b/src/com/android/contacts/editor/CompactContactEditorFragment.java
@@ -159,11 +159,12 @@
     }
 
     @Override
-    protected boolean doSaveAction(int saveMode) {
+    protected boolean doSaveAction(int saveMode, Long joinContactId) {
         final Intent intent = ContactSaveService.createSaveContactIntent(mContext, mState,
                 SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(),
                 ((Activity) mContext).getClass(),
-                CompactContactEditorActivity.ACTION_SAVE_COMPLETED, mUpdatedPhotos);
+                CompactContactEditorActivity.ACTION_SAVE_COMPLETED, mUpdatedPhotos,
+                JOIN_CONTACT_ID_EXTRA_KEY, joinContactId);
         try {
             mContext.startService(intent);
         } catch (Exception exception) {
diff --git a/src/com/android/contacts/editor/ContactEditorBaseFragment.java b/src/com/android/contacts/editor/ContactEditorBaseFragment.java
index 633ee34..bd3f91d 100644
--- a/src/com/android/contacts/editor/ContactEditorBaseFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorBaseFragment.java
@@ -93,6 +93,7 @@
  */
 abstract public class ContactEditorBaseFragment extends Fragment implements
         ContactEditor, SplitContactConfirmationDialogFragment.Listener,
+        JoinContactConfirmationDialogFragment.Listener,
         AggregationSuggestionEngine.Listener, AggregationSuggestionView.Listener,
         CancelEditDialogFragment.Listener {
 
@@ -208,6 +209,11 @@
     public static final String SAVE_MODE_EXTRA_KEY = "saveMode";
 
     /**
+     * Intent extra key for the contact ID to join the current contact to after saving.
+     */
+    public static final String JOIN_CONTACT_ID_EXTRA_KEY = "joinContactId";
+
+    /**
      * Callbacks for Activities that host contact editors Fragments.
      */
     public interface Listener {
@@ -668,7 +674,13 @@
                 if (resultCode != Activity.RESULT_OK) return;
                 if (data != null) {
                     final long contactId = ContentUris.parseId(data.getData());
-                    joinAggregate(contactId);
+                    if (hasPendingChanges()) {
+                        // Ask the user if they want to save changes before doing the join
+                        JoinContactConfirmationDialogFragment.show(this, contactId);
+                    } else {
+                        // Do the join immediately
+                        joinAggregate(contactId);
+                    }
                 }
                 break;
             }
@@ -870,7 +882,7 @@
     }
 
     private boolean doJoinContactAction() {
-        if (!hasValidState()) {
+        if (!hasValidState() || mLookupUri == null) {
             return false;
         }
 
@@ -883,7 +895,13 @@
             return true;
         }
 
-        return save(SaveMode.JOIN);
+        showJoinAggregateActivity(mLookupUri);
+        return true;
+    }
+
+    @Override
+    public void onJoinContactConfirmed(long joinContactId) {
+        doSaveAction(SaveMode.JOIN, joinContactId);
     }
 
     private void doPickRingtone() {
@@ -931,19 +949,22 @@
                 return true;
             }
             onSaveCompleted(/* hadChanges =*/ false, saveMode,
-                    /* saveSucceeded =*/ mLookupUri != null, mLookupUri);
+                    /* saveSucceeded =*/ mLookupUri != null, mLookupUri, /* joinContactId =*/ null);
             return true;
         }
 
         setEnabled(false);
 
-        return doSaveAction(saveMode);
+        return doSaveAction(saveMode, /* joinContactId */ null);
     }
 
     /**
      * Persist the accumulated editor deltas.
+     *
+     * @param joinContactId the raw contact ID to join the contact being saved to after the save,
+     *         may be null.
      */
-    abstract protected boolean doSaveAction(int saveMode);
+    abstract protected boolean doSaveAction(int saveMode, Long joinContactId);
 
     //
     // State accessor methods
@@ -1392,12 +1413,12 @@
 
     @Override
     public void onJoinCompleted(Uri uri) {
-        onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri);
+        onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri, /* joinContactId */ null);
     }
 
     @Override
     public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded,
-            Uri contactLookupUri) {
+            Uri contactLookupUri, Long joinContactId) {
         if (hadChanges) {
             if (saveSucceeded) {
                 switch (saveMode) {
@@ -1438,14 +1459,13 @@
                 if (mListener != null) mListener.onSaveFinished(/* resultIntent= */ null);
                 break;
             }
-            case SaveMode.RELOAD:
             case SaveMode.JOIN:
+                if (saveSucceeded && contactLookupUri != null && joinContactId != null) {
+                    joinAggregate(joinContactId);
+                }
+                break;
+            case SaveMode.RELOAD:
                 if (saveSucceeded && contactLookupUri != null) {
-                    // If it was a JOIN, we are now ready to bring up the join activity.
-                    if (saveMode == SaveMode.JOIN && hasValidState()) {
-                        showJoinAggregateActivity(contactLookupUri);
-                    }
-
                     // If this was in INSERT, we are changing into an EDIT now.
                     // If it already was an EDIT, we are changing to the new Uri now
                     mState = new RawContactDeltaList();
diff --git a/src/com/android/contacts/editor/ContactEditorFragment.java b/src/com/android/contacts/editor/ContactEditorFragment.java
index c17099c..38e43ae 100644
--- a/src/com/android/contacts/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorFragment.java
@@ -421,11 +421,11 @@
     }
 
     @Override
-    protected boolean doSaveAction(int saveMode) {
+    protected boolean doSaveAction(int saveMode, Long joinContactId) {
         final Intent intent = ContactSaveService.createSaveContactIntent(mContext, mState,
                 SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(),
                 ((Activity) mContext).getClass(), ContactEditorActivity.ACTION_SAVE_COMPLETED,
-                mUpdatedPhotos);
+                mUpdatedPhotos, JOIN_CONTACT_ID_EXTRA_KEY, joinContactId);
         mContext.startService(intent);
         return true;
     }
diff --git a/src/com/android/contacts/editor/JoinContactConfirmationDialogFragment.java b/src/com/android/contacts/editor/JoinContactConfirmationDialogFragment.java
new file mode 100644
index 0000000..dc83239
--- /dev/null
+++ b/src/com/android/contacts/editor/JoinContactConfirmationDialogFragment.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2015 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.editor;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.os.Bundle;
+
+import com.android.contacts.R;
+
+/**
+ * Shows a dialog asking the user whether to apply pending changes before joining the contact.
+ */
+public class JoinContactConfirmationDialogFragment extends DialogFragment {
+
+    private static final String ARG_JOIN_CONTACT_ID = "joinContactId";
+
+    /**
+     * Callbacks for the host of this dialog fragment.
+     */
+    public interface Listener {
+
+        /**
+         * Invoked after the user confirms they want to save pending changes before
+         * joining the contact.
+         *
+         * @param joinContactId The raw contact ID of the contact to join to.
+         */
+        void onJoinContactConfirmed(long joinContactId);
+    }
+
+    /**
+     * @param joinContactId The raw contact ID of the contact to join to after confirmation.
+     */
+    public static void show(ContactEditorBaseFragment fragment, long joinContactId) {
+        final Bundle args = new Bundle();
+        args.putLong(ARG_JOIN_CONTACT_ID, joinContactId);
+
+        final JoinContactConfirmationDialogFragment dialog = new
+                JoinContactConfirmationDialogFragment();
+        dialog.setTargetFragment(fragment, 0);
+        dialog.setArguments(args);
+        dialog.show(fragment.getFragmentManager(), "joinContactConfirmationDialog");
+    }
+
+    private long mContactId;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mContactId = getArguments().getLong(ARG_JOIN_CONTACT_ID);
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+        builder.setTitle(R.string.joinConfirmation_title);
+        builder.setIconAttribute(android.R.attr.alertDialogIcon);
+        builder.setMessage(R.string.joinConfirmation);
+        builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                final Listener targetListener = (Listener) getTargetFragment();
+                targetListener.onJoinContactConfirmed(mContactId);
+            }
+        });
+        builder.setNegativeButton(android.R.string.cancel, null);
+        builder.setCancelable(false);
+        return builder.create();
+    }
+}
diff --git a/src/com/android/contacts/quickcontact/InvisibleContactUtil.java b/src/com/android/contacts/quickcontact/InvisibleContactUtil.java
index 3609fbc..706cfdb 100644
--- a/src/com/android/contacts/quickcontact/InvisibleContactUtil.java
+++ b/src/com/android/contacts/quickcontact/InvisibleContactUtil.java
@@ -94,7 +94,8 @@
         final Intent intent = ContactSaveService.createSaveContactIntent(
                 context,
                 contactDeltaList, "", 0, false, QuickContactActivity.class,
-                Intent.ACTION_VIEW, null);
+                Intent.ACTION_VIEW, null, /* joinContactIdExtraKey =*/ null,
+                /* joinContactId =*/ null);
         context.startService(intent);
     }