Move suggest joins popup & activiy to BaseEditContactFragment

Bug 19124091

Change-Id: Ic5a6b8b6359dc9691cfcac2348ba895c542d05d9
diff --git a/src/com/android/contacts/editor/CompactContactEditorFragment.java b/src/com/android/contacts/editor/CompactContactEditorFragment.java
index fbde8c9..4291f91 100644
--- a/src/com/android/contacts/editor/CompactContactEditorFragment.java
+++ b/src/com/android/contacts/editor/CompactContactEditorFragment.java
@@ -75,25 +75,4 @@
         onSaveCompleted(/* hadChanges =*/ false, SaveMode.RELOAD,
                 /* saveSucceeded =*/ uri != null, uri);
     }
-
-    @Override
-    public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded,
-            Uri contactLookupUri) {
-        switch (saveMode) {
-            case SaveMode.CLOSE:
-            case SaveMode.HOME:
-                if (mListener != null) {
-                    final Intent resultIntent;
-                    if (saveSucceeded && contactLookupUri != null) {
-                        final Uri lookupUri = maybeConvertToLegacyLookupUri(
-                                mContext, contactLookupUri, mLookupUri);
-                        resultIntent = composeQuickContactsIntent(mContext, lookupUri);
-                    } else {
-                        resultIntent = null;
-                    }
-                    mListener.onSaveFinished(resultIntent);
-                }
-                break;
-        }
-    }
 }
diff --git a/src/com/android/contacts/editor/ContactEditorBaseFragment.java b/src/com/android/contacts/editor/ContactEditorBaseFragment.java
index e1a841a..d4f5a5a 100644
--- a/src/com/android/contacts/editor/ContactEditorBaseFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorBaseFragment.java
@@ -23,6 +23,7 @@
 import com.android.contacts.GroupMetaDataLoader;
 import com.android.contacts.R;
 import com.android.contacts.activities.ContactEditorAccountsChangedActivity;
+import com.android.contacts.activities.ContactEditorActivity;
 import com.android.contacts.activities.ContactEditorBaseActivity;
 import com.android.contacts.activities.ContactEditorBaseActivity.ContactEditor;
 import com.android.contacts.common.model.AccountTypeManager;
@@ -35,9 +36,12 @@
 import com.android.contacts.common.model.ValuesDelta;
 import com.android.contacts.common.model.account.AccountType;
 import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.editor.AggregationSuggestionEngine.Suggestion;
+import com.android.contacts.list.UiIntentActions;
 import com.android.contacts.quickcontact.QuickContactActivity;
 import com.android.contacts.util.HelpUtils;
 import com.android.contacts.util.PhoneCapabilityTester;
+import com.android.contacts.util.UiClosables;
 
 import android.accounts.Account;
 import android.app.Activity;
@@ -66,11 +70,16 @@
 import android.provider.ContactsContract.QuickContact;
 import android.provider.ContactsContract.RawContacts;
 import android.util.Log;
+import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
 import android.widget.LinearLayout;
+import android.widget.ListPopupWindow;
 import android.widget.Toast;
 
 import java.util.ArrayList;
@@ -80,7 +89,8 @@
  * Base Fragment for contact editors.
  */
 abstract public class ContactEditorBaseFragment extends Fragment implements
-        ContactEditor, SplitContactConfirmationDialogFragment.Listener {
+        ContactEditor, SplitContactConfirmationDialogFragment.Listener,
+        AggregationSuggestionEngine.Listener, AggregationSuggestionView.Listener {
 
     protected static final String TAG = "ContactEditor";
 
@@ -115,6 +125,14 @@
 
     private static final String KEY_ENABLED = "enabled";
 
+    // Aggregation PopupWindow
+    private static final String KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID =
+            "aggregationSuggestionsRawContactId";
+
+    // Join Activity
+    private static final String KEY_CONTACT_ID_FOR_JOIN = "contactidforjoin";
+    private static final String KEY_CONTACT_WRITABLE_FOR_JOIN = "contactwritableforjoin";
+
     protected static final int REQUEST_CODE_JOIN = 0;
     protected static final int REQUEST_CODE_ACCOUNTS_CHANGED = 1;
     protected static final int REQUEST_CODE_PICK_RINGTONE = 2;
@@ -189,6 +207,52 @@
         void onDeleteRequested(Uri contactUri);
     }
 
+    /**
+     * Adapter for aggregation suggestions displayed in a PopupWindow when
+     * editor fields change.
+     */
+    protected static final class AggregationSuggestionAdapter extends BaseAdapter {
+        private final LayoutInflater mLayoutInflater;
+        private final boolean mSetNewContact;
+        private final AggregationSuggestionView.Listener mListener;
+        private final List<AggregationSuggestionEngine.Suggestion> mSuggestions;
+
+        public AggregationSuggestionAdapter(Activity activity, boolean setNewContact,
+                AggregationSuggestionView.Listener listener, List<Suggestion> suggestions) {
+            mLayoutInflater = activity.getLayoutInflater();
+            mSetNewContact = setNewContact;
+            mListener = listener;
+            mSuggestions = suggestions;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            final Suggestion suggestion = (Suggestion) getItem(position);
+            final AggregationSuggestionView suggestionView =
+                    (AggregationSuggestionView) mLayoutInflater.inflate(
+                            R.layout.aggregation_suggestions_item, null);
+            suggestionView.setNewContact(mSetNewContact);
+            suggestionView.setListener(mListener);
+            suggestionView.bindSuggestion(suggestion);
+            return suggestionView;
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+
+        @Override
+        public Object getItem(int position) {
+            return mSuggestions.get(position);
+        }
+
+        @Override
+        public int getCount() {
+            return mSuggestions.size();
+        }
+    }
+
     protected Context mContext;
     protected Listener mListener;
 
@@ -197,6 +261,7 @@
     //
     protected LinearLayout mContent;
     protected View mAggregationSuggestionView;
+    protected ListPopupWindow mAggregationSuggestionPopup;
 
     //
     // Parameters passed in on {@link #load}
@@ -214,6 +279,7 @@
     protected ContactEditorUtils mEditorUtils;
     protected RawContactDeltaComparator mComparator;
     protected ViewIdGenerator mViewIdGenerator;
+    private AggregationSuggestionEngine mAggregationSuggestionEngine;
 
     //
     // Loaded data
@@ -248,6 +314,13 @@
     // Whether editor views and options menu items should be enabled
     private boolean mEnabled = true;
 
+    // Aggregation PopupWindow
+    private long mAggregationSuggestionsRawContactId;
+
+    // Join Activity
+    private long mContactIdForJoin;
+    private boolean mContactWritableForJoin;
+
     //
     // Editor state for {@link ContactEditorView}.
     // (Not saved/restored on rotates)
@@ -372,6 +445,14 @@
             mCustomRingtone = savedState.getString(KEY_CUSTOM_RINGTONE);
 
             mEnabled = savedState.getBoolean(KEY_ENABLED);
+
+            // Aggregation PopupWindow
+            mAggregationSuggestionsRawContactId = savedState.getLong(
+                    KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID);
+
+            // Join Activity
+            mContactIdForJoin = savedState.getLong(KEY_CONTACT_ID_FOR_JOIN);
+            mContactWritableForJoin = savedState.getBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN);
         }
 
         // mState can still be null because it may not have have finished loading before
@@ -481,12 +562,49 @@
 
         outState.putBoolean(KEY_ENABLED, mEnabled);
 
+        // Aggregation PopupWindow
+        outState.putLong(KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID,
+                mAggregationSuggestionsRawContactId);
+
+        // Join Activity
+        outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin);
+        outState.putBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN, mContactWritableForJoin);
+
         super.onSaveInstanceState(outState);
     }
 
     @Override
+    public void onStop() {
+        super.onStop();
+
+        UiClosables.closeQuietly(mAggregationSuggestionPopup);
+
+        // If anything was left unsaved, save it now but keep the editor open.
+        if (!getActivity().isChangingConfigurations() && mStatus == Status.EDITING) {
+            save(SaveMode.RELOAD);
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        if (mAggregationSuggestionEngine != null) {
+            mAggregationSuggestionEngine.quit();
+        }
+    }
+
+    @Override
     public void onActivityResult(int requestCode, int resultCode, Intent data) {
         switch (requestCode) {
+            case REQUEST_CODE_JOIN: {
+                // Ignore failed requests
+                if (resultCode != Activity.RESULT_OK) return;
+                if (data != null) {
+                    final long contactId = ContentUris.parseId(data.getData());
+                    joinAggregate(contactId);
+                }
+                break;
+            }
             case REQUEST_CODE_ACCOUNTS_CHANGED: {
                 // Bail if the account selector was not successful.
                 if (resultCode != Activity.RESULT_OK) {
@@ -643,9 +761,17 @@
 
     @Override
     public void onSplitContactConfirmed() {
-        // When this Fragment is closed we don't want it to auto-save
-        mStatus = Status.CLOSING;
-        if (mListener != null) mListener.onReverted();
+        if (mState.isEmpty()) {
+            // This may happen when this Fragment is recreated by the system during users
+            // confirming the split action (and thus this method is called just before onCreate()),
+            // for example.
+            Log.e(TAG, "mState became null during the user's confirming split action. " +
+                    "Cannot perform the save action.");
+            return;
+        }
+
+        mState.markRawContactsForSplitting();
+        save(SaveMode.SPLIT);;
     }
 
     private boolean doSplitContactAction() {
@@ -947,7 +1073,7 @@
     }
 
     /**
-     * Bind editors using {@link #mState} and other members intialized from the loaded (or new)
+     * Bind editors using {@link #mState} and other members initialized from the loaded (or new)
      * Contact.
      */
     abstract void bindEditors();
@@ -1008,6 +1134,268 @@
         }
     }
 
+    @Override
+    public void onJoinCompleted(Uri uri) {
+        onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri);
+    }
+
+    @Override
+    public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded,
+            Uri contactLookupUri) {
+        if (hadChanges) {
+            if (saveSucceeded) {
+                if (saveMode != SaveMode.JOIN) {
+                    Toast.makeText(mContext, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
+                }
+            } else {
+                Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
+            }
+        }
+        switch (saveMode) {
+            case SaveMode.CLOSE:
+            case SaveMode.HOME:
+                final Intent resultIntent;
+                if (saveSucceeded && contactLookupUri != null) {
+                    final Uri lookupUri = maybeConvertToLegacyLookupUri(
+                            mContext, contactLookupUri, mLookupUri);
+                    resultIntent = composeQuickContactsIntent(mContext, lookupUri);
+                } else {
+                    resultIntent = null;
+                }
+                // It is already saved, so prevent that it is saved again
+                mStatus = Status.CLOSING;
+                if (mListener != null) mListener.onSaveFinished(resultIntent);
+                break;
+
+            case SaveMode.RELOAD:
+            case SaveMode.JOIN:
+                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();
+                    load(Intent.ACTION_EDIT, contactLookupUri, null);
+                    mStatus = Status.LOADING;
+                    getLoaderManager().restartLoader(LOADER_DATA, null, mDataLoaderListener);
+                }
+                break;
+
+            case SaveMode.SPLIT:
+                mStatus = Status.CLOSING;
+                if (mListener != null) {
+                    mListener.onContactSplit(contactLookupUri);
+                } else {
+                    Log.d(TAG, "No listener registered, can not call onSplitFinished");
+                }
+                break;
+        }
+    }
+
+    /**
+     * Shows a list of aggregates that can be joined into the currently viewed aggregate.
+     *
+     * @param contactLookupUri the fresh URI for the currently edited contact (after saving it)
+     */
+    private void showJoinAggregateActivity(Uri contactLookupUri) {
+        if (contactLookupUri == null || !isAdded()) {
+            return;
+        }
+
+        mContactIdForJoin = ContentUris.parseId(contactLookupUri);
+        mContactWritableForJoin = isContactWritable();
+        final Intent intent = new Intent(UiIntentActions.PICK_JOIN_CONTACT_ACTION);
+        intent.putExtra(UiIntentActions.TARGET_CONTACT_ID_EXTRA_KEY, mContactIdForJoin);
+        startActivityForResult(intent, REQUEST_CODE_JOIN);
+    }
+
+    /**
+     * Returns true if there is at least one writable raw contact in the current contact.
+     */
+    private boolean isContactWritable() {
+        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
+        int size = mState.size();
+        for (int i = 0; i < size; i++) {
+            RawContactDelta entity = mState.get(i);
+            final AccountType type = entity.getAccountType(accountTypes);
+            if (type.areContactsWritable()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    //
+    // Aggregation PopupWindow
+    //
+
+    /**
+     * Triggers an asynchronous search for aggregation suggestions.
+     */
+    protected void acquireAggregationSuggestions(Context context,
+            RawContactEditorView rawContactEditor) {
+        long rawContactId = rawContactEditor.getRawContactId();
+        if (mAggregationSuggestionsRawContactId != rawContactId
+                && mAggregationSuggestionView != null) {
+            mAggregationSuggestionView.setVisibility(View.GONE);
+            mAggregationSuggestionView = null;
+            mAggregationSuggestionEngine.reset();
+        }
+
+        mAggregationSuggestionsRawContactId = rawContactId;
+
+        if (mAggregationSuggestionEngine == null) {
+            mAggregationSuggestionEngine = new AggregationSuggestionEngine(context);
+            mAggregationSuggestionEngine.setListener(this);
+            mAggregationSuggestionEngine.start();
+        }
+
+        mAggregationSuggestionEngine.setContactId(getContactId());
+
+        LabeledEditorView nameEditor = rawContactEditor.getNameEditor();
+        mAggregationSuggestionEngine.onNameChange(nameEditor.getValues());
+    }
+
+    /**
+     * Returns the contact ID for the currently edited contact or 0 if the contact is new.
+     */
+    private long getContactId() {
+        for (RawContactDelta rawContact : mState) {
+            Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID);
+            if (contactId != null) {
+                return contactId;
+            }
+        }
+        return 0;
+    }
+
+    @Override
+    public void onAggregationSuggestionChange() {
+        final Activity activity = getActivity();
+        if ((activity != null && activity.isFinishing())
+                || !isVisible() ||  mState.isEmpty() || mStatus != Status.EDITING) {
+            return;
+        }
+
+        UiClosables.closeQuietly(mAggregationSuggestionPopup);
+
+        if (mAggregationSuggestionEngine.getSuggestedContactCount() == 0) {
+            return;
+        }
+
+        final RawContactEditorView rawContactView = (RawContactEditorView)
+                getRawContactEditorView(mAggregationSuggestionsRawContactId);
+        if (rawContactView == null) {
+            return; // Raw contact deleted?
+        }
+        final View anchorView = rawContactView.findViewById(R.id.anchor_view);
+        mAggregationSuggestionPopup = new ListPopupWindow(mContext, null);
+        mAggregationSuggestionPopup.setAnchorView(anchorView);
+        mAggregationSuggestionPopup.setWidth(anchorView.getWidth());
+        mAggregationSuggestionPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
+        mAggregationSuggestionPopup.setAdapter(
+                new AggregationSuggestionAdapter(
+                        getActivity(),
+                        mState.size() == 1 && mState.get(0).isContactInsert(),
+                        /* listener =*/ this,
+                        mAggregationSuggestionEngine.getSuggestions()));
+        mAggregationSuggestionPopup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+            @Override
+            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+                final AggregationSuggestionView suggestionView = (AggregationSuggestionView) view;
+                suggestionView.handleItemClickEvent();
+                UiClosables.closeQuietly(mAggregationSuggestionPopup);
+                mAggregationSuggestionPopup = null;
+            }
+        });
+        mAggregationSuggestionPopup.show();
+    }
+
+    /**
+     * Finds raw contact editor view for the given rawContactId.
+     */
+    private BaseRawContactEditorView getRawContactEditorView(long rawContactId) {
+        for (int i = 0; i < mContent.getChildCount(); i++) {
+            final View childView = mContent.getChildAt(i);
+            if (childView instanceof BaseRawContactEditorView) {
+                final BaseRawContactEditorView editor = (BaseRawContactEditorView) childView;
+                if (editor.getRawContactId() == rawContactId) {
+                    return editor;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Whether the given raw contact ID matches the one used to last load aggregation
+     * suggestions.
+     */
+    protected boolean isAggregationSuggestionRawContactId(long rawContactId) {
+        return mAggregationSuggestionsRawContactId == rawContactId;
+    }
+
+    @Override
+    public void onJoinAction(long contactId, List<Long> rawContactIdList) {
+        final long rawContactIds[] = new long[rawContactIdList.size()];
+        for (int i = 0; i < rawContactIds.length; i++) {
+            rawContactIds[i] = rawContactIdList.get(i);
+        }
+        try {
+            JoinSuggestedContactDialogFragment.show(this, rawContactIds);
+        } catch (Exception ignored) {
+            // No problem - the activity is no longer available to display the dialog
+        }
+    }
+
+    /**
+     * Joins the suggested contact (specified by the id's of constituent raw
+     * contacts), save all changes, and stay in the editor.
+     */
+    protected void doJoinSuggestedContact(long[] rawContactIds) {
+        if (!hasValidState() || mStatus != Status.EDITING) {
+            return;
+        }
+
+        mState.setJoinWithRawContacts(rawContactIds);
+        save(SaveMode.RELOAD);
+    }
+
+    @Override
+    public void onEditAction(Uri contactLookupUri) {
+        SuggestionEditConfirmationDialogFragment.show(this, contactLookupUri);
+    }
+
+    /**
+     * Abandons the currently edited contact and switches to editing the suggested
+     * one, transferring all the data there
+     */
+    protected void doEditSuggestedContact(Uri contactUri) {
+        if (mListener != null) {
+            // make sure we don't save this contact when closing down
+            mStatus = Status.CLOSING;
+            mListener.onEditOtherContactRequested(
+                    contactUri, mState.get(0).getContentValues());
+        }
+    }
+
+    //
+    // Join Activity
+    //
+
+    /**
+     * Performs aggregation with the contact selected by the user from suggestions or A-Z list.
+     */
+    private void joinAggregate(final long contactId) {
+        Intent intent = ContactSaveService.createJoinContactsIntent(mContext, mContactIdForJoin,
+                contactId, mContactWritableForJoin,
+                ContactEditorActivity.class, ContactEditorActivity.ACTION_JOIN_COMPLETED);
+        mContext.startService(intent);
+    }
+
     //
     // Utility methods
     //
diff --git a/src/com/android/contacts/editor/ContactEditorFragment.java b/src/com/android/contacts/editor/ContactEditorFragment.java
index dfa58ac..c71c891 100644
--- a/src/com/android/contacts/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorFragment.java
@@ -17,29 +17,20 @@
 package com.android.contacts.editor;
 
 import android.app.Activity;
-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.content.Intent;
 import android.graphics.Bitmap;
 import android.net.Uri;
 import android.os.Bundle;
 import android.provider.ContactsContract.CommonDataKinds.Photo;
-import android.provider.ContactsContract.RawContacts;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemClickListener;
-import android.widget.BaseAdapter;
 import android.widget.LinearLayout;
 import android.widget.ListPopupWindow;
-import android.widget.Toast;
 
 import com.android.contacts.ContactSaveService;
 import com.android.contacts.R;
@@ -55,9 +46,7 @@
 import com.android.contacts.common.util.AccountsListAdapter;
 import com.android.contacts.common.util.AccountsListAdapter.AccountListFilter;
 import com.android.contacts.detail.PhotoSelectionHandler;
-import com.android.contacts.editor.AggregationSuggestionEngine.Suggestion;
 import com.android.contacts.editor.Editor.EditorListener;
-import com.android.contacts.list.UiIntentActions;
 import com.android.contacts.util.ContactPhotoUtils;
 import com.android.contacts.util.UiClosables;
 
@@ -74,42 +63,19 @@
  */
 public class ContactEditorFragment extends ContactEditorBaseFragment implements
         ContactEditor, SplitContactConfirmationDialogFragment.Listener,
-        AggregationSuggestionEngine.Listener, AggregationSuggestionView.Listener,
         RawContactReadOnlyEditorView.Listener {
 
-    // Joins
-    private static final String KEY_CONTACT_ID_FOR_JOIN = "contactidforjoin";
-    private static final String KEY_CONTACT_WRITABLE_FOR_JOIN = "contactwritableforjoin";
-
     private static final String KEY_EXPANDED_EDITORS = "expandedEditors";
 
-    // Photos
     private static final String KEY_RAW_CONTACT_ID_REQUESTING_PHOTO = "photorequester";
     private static final String KEY_CURRENT_PHOTO_URI = "currentphotouri";
     private static final String KEY_UPDATED_PHOTOS = "updatedPhotos";
 
-    // Aggregations
-    private static final String KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID =
-            "aggregationSuggestionsRawContactId";
-
     public static final String SAVE_MODE_EXTRA_KEY = "saveMode";
 
-    //
-    // Helpers
-    //
-    private AggregationSuggestionEngine mAggregationSuggestionEngine;
-
-    //
-    // Contact editor state
-    //
-    // Joins
-    private long mContactIdForJoin;
-    private boolean mContactWritableForJoin;
-
     // Used to store which raw contact editors have been expanded. Keyed on raw contact ids.
     private HashMap<Long, Boolean> mExpandedEditors = new HashMap<Long, Boolean>();
 
-    // Photos
     /**
      * The raw contact for which we started "take photo" or "choose photo from gallery" most
      * recently.  Used to restore {@link #mCurrentPhotoHandler} after orientation change.
@@ -128,88 +94,10 @@
     private Uri mCurrentPhotoUri;
     private Bundle mUpdatedPhotos = new Bundle();
 
-    // Aggregations
-    private long mAggregationSuggestionsRawContactId;
-    private ListPopupWindow mAggregationSuggestionPopup;
-
-    private static final class AggregationSuggestionAdapter extends BaseAdapter {
-        private final Activity mActivity;
-        private final boolean mSetNewContact;
-        private final AggregationSuggestionView.Listener mListener;
-        private final List<Suggestion> mSuggestions;
-
-        public AggregationSuggestionAdapter(Activity activity, boolean setNewContact,
-                AggregationSuggestionView.Listener listener, List<Suggestion> suggestions) {
-            mActivity = activity;
-            mSetNewContact = setNewContact;
-            mListener = listener;
-            mSuggestions = suggestions;
-        }
-
-        @Override
-        public View getView(int position, View convertView, ViewGroup parent) {
-            Suggestion suggestion = (Suggestion) getItem(position);
-            LayoutInflater inflater = mActivity.getLayoutInflater();
-            AggregationSuggestionView suggestionView =
-                    (AggregationSuggestionView) inflater.inflate(
-                            R.layout.aggregation_suggestions_item, null);
-            suggestionView.setNewContact(mSetNewContact);
-            suggestionView.setListener(mListener);
-            suggestionView.bindSuggestion(suggestion);
-            return suggestionView;
-        }
-
-        @Override
-        public long getItemId(int position) {
-            return position;
-        }
-
-        @Override
-        public Object getItem(int position) {
-            return mSuggestions.get(position);
-        }
-
-        @Override
-        public int getCount() {
-            return mSuggestions.size();
-        }
-    }
-
-    private OnItemClickListener mAggregationSuggestionItemClickListener =
-            new OnItemClickListener() {
-        @Override
-        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
-            final AggregationSuggestionView suggestionView = (AggregationSuggestionView) view;
-            suggestionView.handleItemClickEvent();
-            UiClosables.closeQuietly(mAggregationSuggestionPopup);
-            mAggregationSuggestionPopup = null;
-        }
-    };
-
     public ContactEditorFragment() {
     }
 
     @Override
-    public void onStop() {
-        super.onStop();
-
-        UiClosables.closeQuietly(mAggregationSuggestionPopup);
-
-        // If anything was left unsaved, save it now but keep the editor open.
-        if (!getActivity().isChangingConfigurations() && mStatus == Status.EDITING) {
-            save(SaveMode.RELOAD);
-        }
-    }
-
-    @Override
-    public void onDestroy() {
-        super.onDestroy();
-        if (mAggregationSuggestionEngine != null) {
-            mAggregationSuggestionEngine.quit();
-        }
-    }
-
-    @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
         final View view = inflater.inflate(R.layout.contact_editor_fragment, container, false);
 
@@ -221,32 +109,16 @@
     }
 
     @Override
-    public void onStart() {
-        getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupLoaderListener);
-        super.onStart();
-    }
-
-    @Override
     public void onCreate(Bundle savedState) {
         super.onCreate(savedState);
 
         if (savedState != null) {
-            // Joins
-            mContactIdForJoin = savedState.getLong(KEY_CONTACT_ID_FOR_JOIN);
-            mContactWritableForJoin = savedState.getBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN);
-
             mExpandedEditors = (HashMap<Long, Boolean>)
                     savedState.getSerializable(KEY_EXPANDED_EDITORS);
-
-            // Photos
             mRawContactIdRequestingPhoto = savedState.getLong(
                     KEY_RAW_CONTACT_ID_REQUESTING_PHOTO);
             mCurrentPhotoUri = savedState.getParcelable(KEY_CURRENT_PHOTO_URI);
             mUpdatedPhotos = savedState.getParcelable(KEY_UPDATED_PHOTOS);
-
-            // Aggregations
-            mAggregationSuggestionsRawContactId = savedState.getLong(
-                    KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID);
         }
     }
 
@@ -420,7 +292,7 @@
                         rawContactEditor.getNickNameEditor();
                 nickNameEditor.setEditorListener(listener);
 
-                if (rawContactId == mAggregationSuggestionsRawContactId) {
+                if (isAggregationSuggestionRawContactId(rawContactId)) {
                     acquireAggregationSuggestions(activity, rawContactEditor);
                 }
 
@@ -595,302 +467,11 @@
     }
 
     @Override
-    public void onJoinCompleted(Uri uri) {
-        onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri);
-    }
-
-    @Override
-    public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded,
-            Uri contactLookupUri) {
-        if (hadChanges) {
-            if (saveSucceeded) {
-                if (saveMode != SaveMode.JOIN) {
-                    Toast.makeText(mContext, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
-                }
-            } else {
-                Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
-            }
-        }
-        switch (saveMode) {
-            case SaveMode.CLOSE:
-            case SaveMode.HOME:
-                final Intent resultIntent;
-                if (saveSucceeded && contactLookupUri != null) {
-                    final Uri lookupUri = maybeConvertToLegacyLookupUri(
-                            mContext, contactLookupUri, mLookupUri);
-                    resultIntent = composeQuickContactsIntent(mContext, lookupUri);
-                } else {
-                    resultIntent = null;
-                }
-                // It is already saved, so prevent that it is saved again
-                mStatus = Status.CLOSING;
-                if (mListener != null) mListener.onSaveFinished(resultIntent);
-                break;
-
-            case SaveMode.RELOAD:
-            case SaveMode.JOIN:
-                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();
-                    load(Intent.ACTION_EDIT, contactLookupUri, null);
-                    mStatus = Status.LOADING;
-                    getLoaderManager().restartLoader(LOADER_DATA, null, mDataLoaderListener);
-                }
-                break;
-
-            case SaveMode.SPLIT:
-                mStatus = Status.CLOSING;
-                if (mListener != null) {
-                    mListener.onContactSplit(contactLookupUri);
-                } else {
-                    Log.d(TAG, "No listener registered, can not call onSplitFinished");
-                }
-                break;
-        }
-    }
-
-    /**
-     * Shows a list of aggregates that can be joined into the currently viewed aggregate.
-     *
-     * @param contactLookupUri the fresh URI for the currently edited contact (after saving it)
-     */
-    private void showJoinAggregateActivity(Uri contactLookupUri) {
-        if (contactLookupUri == null || !isAdded()) {
-            return;
-        }
-
-        mContactIdForJoin = ContentUris.parseId(contactLookupUri);
-        mContactWritableForJoin = isContactWritable();
-        final Intent intent = new Intent(UiIntentActions.PICK_JOIN_CONTACT_ACTION);
-        intent.putExtra(UiIntentActions.TARGET_CONTACT_ID_EXTRA_KEY, mContactIdForJoin);
-        startActivityForResult(intent, REQUEST_CODE_JOIN);
-    }
-
-    /**
-     * Performs aggregation with the contact selected by the user from suggestions or A-Z list.
-     */
-    private void joinAggregate(final long contactId) {
-        Intent intent = ContactSaveService.createJoinContactsIntent(mContext, mContactIdForJoin,
-                contactId, mContactWritableForJoin,
-                ContactEditorActivity.class, ContactEditorActivity.ACTION_JOIN_COMPLETED);
-        mContext.startService(intent);
-    }
-
-    /**
-     * Returns true if there is at least one writable raw contact in the current contact.
-     */
-    private boolean isContactWritable() {
-        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
-        int size = mState.size();
-        for (int i = 0; i < size; i++) {
-            RawContactDelta entity = mState.get(i);
-            final AccountType type = entity.getAccountType(accountTypes);
-            if (type.areContactsWritable()) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Returns the contact ID for the currently edited contact or 0 if the contact is new.
-     */
-    protected long getContactId() {
-        for (RawContactDelta rawContact : mState) {
-            Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID);
-            if (contactId != null) {
-                return contactId;
-            }
-        }
-        return 0;
-    }
-
-    /**
-     * Triggers an asynchronous search for aggregation suggestions.
-     */
-    private void acquireAggregationSuggestions(Context context,
-            RawContactEditorView rawContactEditor) {
-        long rawContactId = rawContactEditor.getRawContactId();
-        if (mAggregationSuggestionsRawContactId != rawContactId
-                && mAggregationSuggestionView != null) {
-            mAggregationSuggestionView.setVisibility(View.GONE);
-            mAggregationSuggestionView = null;
-            mAggregationSuggestionEngine.reset();
-        }
-
-        mAggregationSuggestionsRawContactId = rawContactId;
-
-        if (mAggregationSuggestionEngine == null) {
-            mAggregationSuggestionEngine = new AggregationSuggestionEngine(context);
-            mAggregationSuggestionEngine.setListener(this);
-            mAggregationSuggestionEngine.start();
-        }
-
-        mAggregationSuggestionEngine.setContactId(getContactId());
-
-        LabeledEditorView nameEditor = rawContactEditor.getNameEditor();
-        mAggregationSuggestionEngine.onNameChange(nameEditor.getValues());
-    }
-
-    @Override
-    public void onAggregationSuggestionChange() {
-        Activity activity = getActivity();
-        if ((activity != null && activity.isFinishing())
-                || !isVisible() ||  mState.isEmpty() || mStatus != Status.EDITING) {
-            return;
-        }
-
-        UiClosables.closeQuietly(mAggregationSuggestionPopup);
-
-        if (mAggregationSuggestionEngine.getSuggestedContactCount() == 0) {
-            return;
-        }
-
-        final RawContactEditorView rawContactView =
-                (RawContactEditorView)getRawContactEditorView(mAggregationSuggestionsRawContactId);
-        if (rawContactView == null) {
-            return; // Raw contact deleted?
-        }
-        final View anchorView = rawContactView.findViewById(R.id.anchor_view);
-        mAggregationSuggestionPopup = new ListPopupWindow(mContext, null);
-        mAggregationSuggestionPopup.setAnchorView(anchorView);
-        mAggregationSuggestionPopup.setWidth(anchorView.getWidth());
-        mAggregationSuggestionPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
-        mAggregationSuggestionPopup.setAdapter(
-                new AggregationSuggestionAdapter(getActivity(),
-                        mState.size() == 1 && mState.get(0).isContactInsert(),
-                        this, mAggregationSuggestionEngine.getSuggestions()));
-        mAggregationSuggestionPopup.setOnItemClickListener(mAggregationSuggestionItemClickListener);
-        mAggregationSuggestionPopup.show();
-    }
-
-    @Override
-    public void onJoinAction(long contactId, List<Long> rawContactIdList) {
-        long rawContactIds[] = new long[rawContactIdList.size()];
-        for (int i = 0; i < rawContactIds.length; i++) {
-            rawContactIds[i] = rawContactIdList.get(i);
-        }
-        JoinSuggestedContactDialogFragment dialog =
-                new JoinSuggestedContactDialogFragment();
-        Bundle args = new Bundle();
-        args.putLongArray("rawContactIds", rawContactIds);
-        dialog.setArguments(args);
-        dialog.setTargetFragment(this, 0);
-        try {
-            dialog.show(getFragmentManager(), "join");
-        } catch (Exception ex) {
-            // No problem - the activity is no longer available to display the dialog
-        }
-    }
-
-    public static class JoinSuggestedContactDialogFragment extends DialogFragment {
-
-        @Override
-        public Dialog onCreateDialog(Bundle savedInstanceState) {
-            return new AlertDialog.Builder(getActivity())
-                    .setIconAttribute(android.R.attr.alertDialogIcon)
-                    .setMessage(R.string.aggregation_suggestion_join_dialog_message)
-                    .setPositiveButton(android.R.string.yes,
-                        new DialogInterface.OnClickListener() {
-                            @Override
-                            public void onClick(DialogInterface dialog, int whichButton) {
-                                ContactEditorFragment targetFragment =
-                                        (ContactEditorFragment) getTargetFragment();
-                                long rawContactIds[] =
-                                        getArguments().getLongArray("rawContactIds");
-                                targetFragment.doJoinSuggestedContact(rawContactIds);
-                            }
-                        }
-                    )
-                    .setNegativeButton(android.R.string.no, null)
-                    .create();
-        }
-    }
-
-    /**
-     * Joins the suggested contact (specified by the id's of constituent raw
-     * contacts), save all changes, and stay in the editor.
-     */
-    protected void doJoinSuggestedContact(long[] rawContactIds) {
-        if (!hasValidState() || mStatus != Status.EDITING) {
-            return;
-        }
-
-        mState.setJoinWithRawContacts(rawContactIds);
-        save(SaveMode.RELOAD);
-    }
-
-    @Override
-    public void onEditAction(Uri contactLookupUri) {
-        SuggestionEditConfirmationDialogFragment dialog =
-                new SuggestionEditConfirmationDialogFragment();
-        Bundle args = new Bundle();
-        args.putParcelable("contactUri", contactLookupUri);
-        dialog.setArguments(args);
-        dialog.setTargetFragment(this, 0);
-        dialog.show(getFragmentManager(), "edit");
-    }
-
-    public static class SuggestionEditConfirmationDialogFragment extends DialogFragment {
-
-        @Override
-        public Dialog onCreateDialog(Bundle savedInstanceState) {
-            return new AlertDialog.Builder(getActivity())
-                    .setIconAttribute(android.R.attr.alertDialogIcon)
-                    .setMessage(R.string.aggregation_suggestion_edit_dialog_message)
-                    .setPositiveButton(android.R.string.yes,
-                        new DialogInterface.OnClickListener() {
-                            @Override
-                            public void onClick(DialogInterface dialog, int whichButton) {
-                                ContactEditorFragment targetFragment =
-                                        (ContactEditorFragment) getTargetFragment();
-                                Uri contactUri =
-                                        getArguments().getParcelable("contactUri");
-                                targetFragment.doEditSuggestedContact(contactUri);
-                            }
-                        }
-                    )
-                    .setNegativeButton(android.R.string.no, null)
-                    .create();
-        }
-    }
-
-    /**
-     * Abandons the currently edited contact and switches to editing the suggested
-     * one, transferring all the data there
-     */
-    protected void doEditSuggestedContact(Uri contactUri) {
-        if (mListener != null) {
-            // make sure we don't save this contact when closing down
-            mStatus = Status.CLOSING;
-            mListener.onEditOtherContactRequested(
-                    contactUri, mState.get(0).getContentValues());
-        }
-    }
-
-    @Override
     public void onSaveInstanceState(Bundle outState) {
-        // Joins
-        outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin);
-        outState.putBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN, mContactWritableForJoin);
-
         outState.putSerializable(KEY_EXPANDED_EDITORS, mExpandedEditors);
-
-        // Photos
         outState.putLong(KEY_RAW_CONTACT_ID_REQUESTING_PHOTO, mRawContactIdRequestingPhoto);
         outState.putParcelable(KEY_CURRENT_PHOTO_URI, mCurrentPhotoUri);
         outState.putParcelable(KEY_UPDATED_PHOTOS, mUpdatedPhotos);
-
-        // Aggregations
-        outState.putLong(KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID,
-                mAggregationSuggestionsRawContactId);
-
         super.onSaveInstanceState(outState);
     }
 
@@ -906,21 +487,7 @@
             return;
         }
 
-        switch (requestCode) {
-            case REQUEST_CODE_JOIN: {
-                // Ignore failed requests
-                if (resultCode != Activity.RESULT_OK) return;
-                if (data != null) {
-                    final long contactId = ContentUris.parseId(data.getData());
-                    joinAggregate(contactId);
-                }
-                break;
-            }
-            default: {
-                super.onActivityResult(requestCode, resultCode, data);
-                break;
-            }
-        }
+        super.onActivityResult(requestCode, resultCode, data);
     }
 
     /**
@@ -1002,21 +569,6 @@
         return false;
     }
 
-    @Override
-    public void onSplitContactConfirmed() {
-        if (mState.isEmpty()) {
-            // This may happen when this Fragment is recreated by the system during users
-            // confirming the split action (and thus this method is called just before onCreate()),
-            // for example.
-            Log.e(TAG, "mState became null during the user's confirming split action. " +
-                    "Cannot perform the save action.");
-            return;
-        }
-
-        mState.markRawContactsForSplitting();
-        save(SaveMode.SPLIT);
-    }
-
     /**
      * Custom photo handler for the editor.  The inner listener that this creates also has a
      * reference to the editor and acts as an {@link EditorListener}, and uses that editor to hold
diff --git a/src/com/android/contacts/editor/JoinSuggestedContactDialogFragment.java b/src/com/android/contacts/editor/JoinSuggestedContactDialogFragment.java
new file mode 100644
index 0000000..dca6e18
--- /dev/null
+++ b/src/com/android/contacts/editor/JoinSuggestedContactDialogFragment.java
@@ -0,0 +1,61 @@
+/*
+ * 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 com.android.contacts.R;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.os.Bundle;
+
+public class JoinSuggestedContactDialogFragment extends DialogFragment {
+
+    private static final String ARG_RAW_CONTACT_IDS = "rawContactIds";
+
+    public static void show(ContactEditorBaseFragment fragment, long[] rawContactIds) {
+        final Bundle args = new Bundle();
+        args.putLongArray(ARG_RAW_CONTACT_IDS, rawContactIds);
+
+        final JoinSuggestedContactDialogFragment dialog = new JoinSuggestedContactDialogFragment();
+        dialog.setArguments(args);
+        dialog.setTargetFragment(fragment, 0);
+        dialog.show(fragment.getFragmentManager(), "join");
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        return new AlertDialog.Builder(getActivity())
+                .setIconAttribute(android.R.attr.alertDialogIcon)
+                .setMessage(R.string.aggregation_suggestion_join_dialog_message)
+                .setPositiveButton(android.R.string.yes,
+                        new DialogInterface.OnClickListener() {
+                            @Override
+                            public void onClick(DialogInterface dialog, int whichButton) {
+                                ContactEditorFragment targetFragment =
+                                        (ContactEditorFragment) getTargetFragment();
+                                long rawContactIds[] =
+                                        getArguments().getLongArray(ARG_RAW_CONTACT_IDS);
+                                targetFragment.doJoinSuggestedContact(rawContactIds);
+                            }
+                        }
+                )
+                .setNegativeButton(android.R.string.no, null)
+                .create();
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/editor/SuggestionEditConfirmationDialogFragment.java b/src/com/android/contacts/editor/SuggestionEditConfirmationDialogFragment.java
new file mode 100644
index 0000000..0af2d2d
--- /dev/null
+++ b/src/com/android/contacts/editor/SuggestionEditConfirmationDialogFragment.java
@@ -0,0 +1,63 @@
+/*
+ * 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 com.android.contacts.R;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.net.Uri;
+import android.os.Bundle;
+
+public class SuggestionEditConfirmationDialogFragment extends DialogFragment {
+
+    private static final String ARG_CONTACT_URI = "contactUri";
+
+    public static void show(ContactEditorBaseFragment fragment, Uri contactUri) {
+        final Bundle args = new Bundle();
+        args.putParcelable(ARG_CONTACT_URI, contactUri);
+
+        final SuggestionEditConfirmationDialogFragment dialog = new
+                SuggestionEditConfirmationDialogFragment();
+        dialog.setArguments(args);
+        dialog.setTargetFragment(fragment, 0);
+        dialog.show(fragment.getFragmentManager(), "edit");
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        return new AlertDialog.Builder(getActivity())
+                .setIconAttribute(android.R.attr.alertDialogIcon)
+                .setMessage(R.string.aggregation_suggestion_edit_dialog_message)
+                .setPositiveButton(android.R.string.yes,
+                        new DialogInterface.OnClickListener() {
+                            @Override
+                            public void onClick(DialogInterface dialog, int whichButton) {
+                                final ContactEditorFragment targetFragment =
+                                        (ContactEditorFragment) getTargetFragment();
+                                final Uri contactUri =
+                                        getArguments().getParcelable(ARG_CONTACT_URI);
+                                targetFragment.doEditSuggestedContact(contactUri);
+                            }
+                        }
+                )
+                .setNegativeButton(android.R.string.no, null)
+                .create();
+    }
+}