Move data loading and account creation to edit contact base class

While moving things from ContactEditorFragment to ContactEditorBaseFragment:

* The data loader callbacks, #onActivityCreated, #setData, and account
  creation methods were not changed in any way
* For the restoration of contact editor state, one small change was made.
  A new key, KEY_AUTO_ADD_TO_DEFAULT_GROUP, was added because the other
  options passed in via intentExtras argument to #load are saved
  so this looks like an oversight in the existing version.

Bug 19124091

Change-Id: I7973f9c5703082f6d8db53c9db22d08a230c5876
diff --git a/src/com/android/contacts/editor/CompactContactEditorFragment.java b/src/com/android/contacts/editor/CompactContactEditorFragment.java
index 5361155..46f50d2 100644
--- a/src/com/android/contacts/editor/CompactContactEditorFragment.java
+++ b/src/com/android/contacts/editor/CompactContactEditorFragment.java
@@ -16,15 +16,19 @@
 
 package com.android.contacts.editor;
 
+import com.google.common.collect.ImmutableList;
+
 import com.android.contacts.R;
 import com.android.contacts.activities.ContactEditorBaseActivity.ContactEditor;
-import com.android.contacts.editor.ContactEditorBaseFragment.Listener;
+import com.android.contacts.common.model.Contact;
+import com.android.contacts.common.model.RawContact;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountWithDataSet;
 
-import android.app.Activity;
-import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -36,21 +40,6 @@
 public class CompactContactEditorFragment extends ContactEditorBaseFragment
         implements ContactEditor {
 
-    private Context mContext;
-    private Listener mListener;
-
-    private String mAction;
-    private Uri mLookupUri;
-    private Bundle mIntentExtras;
-
-    private LinearLayout mContent;
-
-    @Override
-    public void onAttach(Activity activity) {
-        super.onAttach(activity);
-        mContext = activity;
-    }
-
     @Override
     public void setListener(Listener listener) {
         mListener = listener;
@@ -64,6 +53,36 @@
         return view;
     }
 
+    //
+    // ContactEditorBaseFragment
+    //
+
+    @Override
+    protected void bindEditorsForExistingContact(String displayName, boolean isUserProfile,
+            ImmutableList<RawContact> rawContacts) {
+    }
+
+    @Override
+    protected void bindEditorsForNewContact(AccountWithDataSet account,
+            AccountType accountType) {
+    }
+
+    @Override
+    protected void bindEditors() {
+    }
+
+    @Override
+    protected void bindGroupMetaData() {
+    }
+
+    @Override
+    protected void bindMenuItemsForPhone(Contact contact) {
+    }
+
+    //
+    // ContactEditor
+    //
+
     @Override
     public void load(String action, Uri lookupUri, Bundle intentExtras) {
         mAction = action;
diff --git a/src/com/android/contacts/editor/ContactEditorBaseFragment.java b/src/com/android/contacts/editor/ContactEditorBaseFragment.java
index 240a42f..70d5be2 100644
--- a/src/com/android/contacts/editor/ContactEditorBaseFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorBaseFragment.java
@@ -16,28 +16,81 @@
 
 package com.android.contacts.editor;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+import com.android.contacts.GroupMetaDataLoader;
+import com.android.contacts.activities.ContactEditorAccountsChangedActivity;
+import com.android.contacts.activities.ContactEditorBaseActivity;
+import com.android.contacts.activities.ContactEditorBaseActivity.ContactEditor;
+import com.android.contacts.common.model.AccountTypeManager;
+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.account.AccountWithDataSet;
 import com.android.contacts.quickcontact.QuickContactActivity;
 
+import android.accounts.Account;
+import android.app.Activity;
 import android.app.Fragment;
+import android.app.LoaderManager;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
+import android.content.CursorLoader;
 import android.content.Intent;
+import android.content.Loader;
+import android.database.Cursor;
 import android.graphics.Rect;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.SystemClock;
+import android.provider.ContactsContract;
 import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Intents;
 import android.provider.ContactsContract.QuickContact;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
+import android.widget.LinearLayout;
 
 import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Base Fragment for contact editors.
  */
-abstract public class ContactEditorBaseFragment extends Fragment {
+abstract public class ContactEditorBaseFragment extends Fragment implements
+        ContactEditor {
 
-    protected static final String TAG = "ContactCompactEditor";
+    protected static final String TAG = "ContactEditor";
+
+    protected static final int LOADER_DATA = 1;
+    protected static final int LOADER_GROUPS = 2;
+
+    private static final String KEY_ACTION = "action";
+    private static final String KEY_URI = "uri";
+    private static final String KEY_AUTO_ADD_TO_DEFAULT_GROUP = "autoAddToDefaultGroup";
+    private static final String KEY_DISABLE_DELETE_MENU_OPTION = "disableDeleteMenuOption";
+    private static final String KEY_NEW_LOCAL_PROFILE = "newLocalProfile";
+
+    private static final String KEY_VIEW_ID_GENERATOR = "viewidgenerator";
+
+    private static final String KEY_RAW_CONTACTS = "rawContacts";
+
+    private static final String KEY_EDIT_STATE = "state";
+    private static final String KEY_STATUS = "status";
+
+    private static final String KEY_HAS_NEW_CONTACT = "hasNewContact";
+    private static final String KEY_NEW_CONTACT_READY = "newContactDataReady";
+
+    private static final String KEY_IS_EDIT = "isEdit";
+    private static final String KEY_EXISTING_CONTACT_READY = "existingContactDataReady";
+
+    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;
 
     /**
      * Callbacks for Activities that host contact editors Fragments.
@@ -98,6 +151,384 @@
         void onDeleteRequested(Uri contactUri);
     }
 
+    protected Context mContext;
+    protected Listener mListener;
+
+    protected LinearLayout mContent;
+
+    //
+    // Parameters passed in on {@link #load}
+    //
+    protected String mAction;
+    protected Uri mLookupUri;
+    protected Bundle mIntentExtras;
+    protected boolean mAutoAddToDefaultGroup;
+    protected boolean mDisableDeleteMenuOption;
+    protected boolean mNewLocalProfile;
+
+    //
+    // Helpers
+    //
+    protected ContactEditorUtils mEditorUtils;
+    protected RawContactDeltaComparator mComparator;
+    protected ViewIdGenerator mViewIdGenerator;
+
+    //
+    // Loaded data
+    //
+    // Used to temporarily store existing contact data during a rebind call (i.e. account switch)
+    protected ImmutableList<RawContact> mRawContacts;
+    protected Cursor mGroupMetaData;
+
+    //
+    // Contact editor state
+    //
+    protected RawContactDeltaList mState;
+    protected int mStatus;
+
+    // Whether to show the new contact blank form and if it's corresponding delta is ready.
+    protected boolean mHasNewContact;
+    protected boolean mNewContactDataReady;
+
+    // Whether it's an edit of existing contact and if it's corresponding delta is ready.
+    protected boolean mIsEdit;
+    protected boolean mExistingContactDataReady;
+
+    /**
+     * The contact data loader listener.
+     */
+    protected final LoaderManager.LoaderCallbacks<Contact> mDataLoaderListener =
+            new LoaderManager.LoaderCallbacks<Contact>() {
+
+                protected long mLoaderStartTime;
+
+                @Override
+                public Loader<Contact> onCreateLoader(int id, Bundle args) {
+                    mLoaderStartTime = SystemClock.elapsedRealtime();
+                    return new ContactLoader(mContext, mLookupUri, true);
+                }
+
+                @Override
+                public void onLoadFinished(Loader<Contact> loader, Contact data) {
+                    final long loaderCurrentTime = SystemClock.elapsedRealtime();
+                    Log.v(TAG, "Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime));
+                    if (!data.isLoaded()) {
+                        // Item has been deleted. Close activity without saving again.
+                        Log.i(TAG, "No contact found. Closing activity");
+                        mStatus = Status.CLOSING;
+                        if (mListener != null) mListener.onContactNotFound();
+                        return;
+                    }
+
+                    mStatus = Status.EDITING;
+                    mLookupUri = data.getLookupUri();
+                    final long setDataStartTime = SystemClock.elapsedRealtime();
+                    setData(data);
+                    final long setDataEndTime = SystemClock.elapsedRealtime();
+
+                    Log.v(TAG, "Time needed for setting UI: " + (setDataEndTime - setDataStartTime));
+                }
+
+                @Override
+                public void onLoaderReset(Loader<Contact> loader) {
+                }
+            };
+
+    /**
+     * The group meta data loader listener.
+     */
+    protected final LoaderManager.LoaderCallbacks<Cursor> mGroupLoaderListener =
+            new LoaderManager.LoaderCallbacks<Cursor>() {
+
+                @Override
+                public CursorLoader onCreateLoader(int id, Bundle args) {
+                    return new GroupMetaDataLoader(mContext, ContactsContract.Groups.CONTENT_URI);
+                }
+
+                @Override
+                public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+                    mGroupMetaData = data;
+                    bindGroupMetaData();
+                }
+
+                @Override
+                public void onLoaderReset(Loader<Cursor> loader) {
+                }
+            };
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+        mContext = activity;
+        mEditorUtils = ContactEditorUtils.getInstance(mContext);
+        mComparator = new RawContactDeltaComparator(mContext);
+    }
+
+    @Override
+    public void onCreate(Bundle savedState) {
+        if (savedState != null) {
+            // Restore mUri before calling super.onCreate so that onInitializeLoaders
+            // would already have a uri and an action to work with
+            mAction = savedState.getString(KEY_ACTION);
+            mLookupUri = savedState.getParcelable(KEY_URI);
+        }
+
+        super.onCreate(savedState);
+
+        if (savedState == null) {
+            mViewIdGenerator = new ViewIdGenerator();
+        } else {
+            mViewIdGenerator = savedState.getParcelable(KEY_VIEW_ID_GENERATOR);
+
+            mAutoAddToDefaultGroup = savedState.getBoolean(KEY_AUTO_ADD_TO_DEFAULT_GROUP);
+            mDisableDeleteMenuOption = savedState.getBoolean(KEY_DISABLE_DELETE_MENU_OPTION);
+            mNewLocalProfile = savedState.getBoolean(KEY_NEW_LOCAL_PROFILE);
+
+            mRawContacts = ImmutableList.copyOf(savedState.<RawContact>getParcelableArrayList(
+                    KEY_RAW_CONTACTS));
+            // NOTE: mGroupMetaData is not saved/restored
+
+            // Read state from savedState. No loading involved here
+            mState = savedState.<RawContactDeltaList> getParcelable(KEY_EDIT_STATE);
+            mStatus = savedState.getInt(KEY_STATUS);
+
+            mHasNewContact = savedState.getBoolean(KEY_HAS_NEW_CONTACT);
+            mNewContactDataReady = savedState.getBoolean(KEY_NEW_CONTACT_READY);
+
+            mIsEdit = savedState.getBoolean(KEY_IS_EDIT);
+            mExistingContactDataReady = savedState.getBoolean(KEY_EXISTING_CONTACT_READY);
+        }
+
+        // mState can still be null because it may not have have finished loading before
+        // onSaveInstanceState was called.
+        if (mState == null) {
+            mState = new RawContactDeltaList();
+        }
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+
+        validateAction(mAction);
+
+        if (mState.isEmpty()) {
+            // The delta list may not have finished loading before orientation change happens.
+            // In this case, there will be a saved state but deltas will be missing.  Reload from
+            // database.
+            if (Intent.ACTION_EDIT.equals(mAction)) {
+                // Either...
+                // 1) orientation change but load never finished.
+                // or
+                // 2) not an orientation change.  data needs to be loaded for first time.
+                getLoaderManager().initLoader(LOADER_DATA, null, mDataLoaderListener);
+            }
+        } else {
+            // Orientation change, we already have mState, it was loaded by onCreate
+            bindEditors();
+        }
+
+        // Handle initial actions only when existing state missing
+        if (savedInstanceState == null) {
+            if (Intent.ACTION_EDIT.equals(mAction)) {
+                mIsEdit = true;
+            } else if (Intent.ACTION_INSERT.equals(mAction)) {
+                mHasNewContact = true;
+                final Account account = mIntentExtras == null ? null :
+                        (Account) mIntentExtras.getParcelable(Intents.Insert.EXTRA_ACCOUNT);
+                final String dataSet = mIntentExtras == null ? null :
+                        mIntentExtras.getString(Intents.Insert.EXTRA_DATA_SET);
+
+                if (account != null) {
+                    // Account specified in Intent
+                    createContact(new AccountWithDataSet(account.name, account.type, dataSet));
+                } else {
+                    // No Account specified. Let the user choose
+                    // Load Accounts async so that we can present them
+                    selectAccountAndCreateContact();
+                }
+            }
+        }
+    }
+
+    /**
+     * Checks if the requested action is valid.
+     *
+     * @param action The action to test.
+     * @throws IllegalArgumentException when the action is invalid.
+     */
+    private static void validateAction(String action) {
+        if (Intent.ACTION_EDIT.equals(action) || Intent.ACTION_INSERT.equals(action) ||
+                ContactEditorBaseActivity.ACTION_SAVE_COMPLETED.equals(action)) {
+            return;
+        }
+        throw new IllegalArgumentException("Unknown Action String " + action +
+                ". Only support " + Intent.ACTION_EDIT + " or " + Intent.ACTION_INSERT + " or " +
+                ContactEditorBaseActivity.ACTION_SAVE_COMPLETED);
+    }
+
+    @Override
+    public void onStart() {
+        getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupLoaderListener);
+        super.onStart();
+    }
+
+    @Override
+    public void
+    onSaveInstanceState(Bundle outState) {
+        outState.putString(KEY_ACTION, mAction);
+        outState.putParcelable(KEY_URI, mLookupUri);
+        outState.putBoolean(KEY_AUTO_ADD_TO_DEFAULT_GROUP, mAutoAddToDefaultGroup);
+        outState.putBoolean(KEY_DISABLE_DELETE_MENU_OPTION, mDisableDeleteMenuOption);
+        outState.putBoolean(KEY_NEW_LOCAL_PROFILE, mNewLocalProfile);
+
+        outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator);
+
+        outState.putParcelableArrayList(KEY_RAW_CONTACTS, mRawContacts == null ?
+                Lists.<RawContact>newArrayList() : Lists.newArrayList(mRawContacts));
+        // NOTE: mGroupMetaData is not saved
+
+        if (hasValidState()) {
+            // Store entities with modifications
+            outState.putParcelable(KEY_EDIT_STATE, mState);
+        }
+        outState.putInt(KEY_STATUS, mStatus);
+        outState.putBoolean(KEY_HAS_NEW_CONTACT, mHasNewContact);
+        outState.putBoolean(KEY_NEW_CONTACT_READY, mNewContactDataReady);
+        outState.putBoolean(KEY_IS_EDIT, mIsEdit);
+        outState.putBoolean(KEY_EXISTING_CONTACT_READY, mExistingContactDataReady);
+
+        super.onSaveInstanceState(outState);
+    }
+
+    /**
+     * Check if our internal {@link #mState} is valid, usually checked before
+     * performing user actions.
+     */
+    protected boolean hasValidState() {
+        return mState.size() > 0;
+    }
+
+    private void setData(Contact contact) {
+
+        // If we have already loaded data, we do not want to change it here to not confuse the user
+        if (!mState.isEmpty()) {
+            Log.v(TAG, "Ignoring background change. This will have to be rebased later");
+            return;
+        }
+
+        // See if this edit operation needs to be redirected to a custom editor
+        mRawContacts = contact.getRawContacts();
+        if (mRawContacts.size() == 1) {
+            RawContact rawContact = mRawContacts.get(0);
+            String type = rawContact.getAccountTypeString();
+            String dataSet = rawContact.getDataSet();
+            AccountType accountType = rawContact.getAccountType(mContext);
+            if (accountType.getEditContactActivityClassName() != null &&
+                    !accountType.areContactsWritable()) {
+                if (mListener != null) {
+                    String name = rawContact.getAccountName();
+                    long rawContactId = rawContact.getId();
+                    mListener.onCustomEditContactActivityRequested(
+                            new AccountWithDataSet(name, type, dataSet),
+                            ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
+                            mIntentExtras, true);
+                }
+                return;
+            }
+        }
+
+        String displayName = null;
+        // Check for writable raw contacts.  If there are none, then we need to create one so user
+        // can edit.  For the user profile case, there is already an editable contact.
+        if (!contact.isUserProfile() && !contact.isWritableContact(mContext)) {
+            mHasNewContact = true;
+
+            // This is potentially an asynchronous call and will add deltas to list.
+            selectAccountAndCreateContact();
+            displayName = contact.getDisplayName();
+        }
+
+        // This also adds deltas to list
+        // If displayName is null at this point it is simply ignored later on by the editor.
+        bindEditorsForExistingContact(displayName, contact.isUserProfile(),
+                mRawContacts);
+
+        bindMenuItemsForPhone(contact);
+    }
+
+    //
+    // Account creation
+    //
+
+    private void selectAccountAndCreateContact() {
+        // If this is a local profile, then skip the logic about showing the accounts changed
+        // activity and create a phone-local contact.
+        if (mNewLocalProfile) {
+            createContact(null);
+            return;
+        }
+
+        // If there is no default account or the accounts have changed such that we need to
+        // prompt the user again, then launch the account prompt.
+        if (mEditorUtils.shouldShowAccountChangedNotification()) {
+            Intent intent = new Intent(mContext, ContactEditorAccountsChangedActivity.class);
+            mStatus = Status.SUB_ACTIVITY;
+            startActivityForResult(intent, REQUEST_CODE_ACCOUNTS_CHANGED);
+        } else {
+            // Otherwise, there should be a default account. Then either create a local contact
+            // (if default account is null) or create a contact with the specified account.
+            AccountWithDataSet defaultAccount = mEditorUtils.getDefaultAccount();
+            createContact(defaultAccount);
+        }
+    }
+
+    /**
+     * Create a contact by automatically selecting the first account. If there's no available
+     * account, a device-local contact should be created.
+     */
+    protected void createContact() {
+        final List<AccountWithDataSet> accounts =
+                AccountTypeManager.getInstance(mContext).getAccounts(true);
+        // No Accounts available. Create a phone-local contact.
+        if (accounts.isEmpty()) {
+            createContact(null);
+            return;
+        }
+
+        // We have an account switcher in "create-account" screen, so don't need to ask a user to
+        // select an account here.
+        createContact(accounts.get(0));
+    }
+
+    /**
+     * Shows account creation screen associated with a given account.
+     *
+     * @param account may be null to signal a device-local contact should be created.
+     */
+    protected void createContact(AccountWithDataSet account) {
+        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
+        final AccountType accountType = accountTypes.getAccountTypeForAccount(account);
+
+        if (accountType.getCreateContactActivityClassName() != null) {
+            if (mListener != null) {
+                mListener.onCustomCreateContactActivityRequested(account, mIntentExtras);
+            }
+        } else {
+            bindEditorsForNewContact(account, accountType);
+        }
+    }
+
+    // TODO: add javadocs after these are finalized
+    abstract protected void bindEditorsForExistingContact(String displayName, boolean isUserProfile,
+            ImmutableList<RawContact> rawContacts);
+    abstract protected void bindEditorsForNewContact(AccountWithDataSet account,
+            final AccountType accountType);
+    abstract protected void bindEditors();
+    abstract protected void bindGroupMetaData();
+    // TODO: should be removed after options menu is moved to base
+    abstract protected void bindMenuItemsForPhone(Contact contact);
+
     /**
      * Returns a legacy version of the given contactLookupUri if a legacy Uri was originally
      * passed to the contact editor.
@@ -106,6 +537,7 @@
      * @param requestLookupUri The lookup Uri originally passed to the contact editor
      *                         (via Intent data), may be null.
      */
+    // TODO: move to ContactEditorUtils?
     protected static Uri maybeConvertToLegacyLookupUri(Context context, Uri contactLookupUri,
             Uri requestLookupUri) {
         final String legacyAuthority = "contacts";
@@ -126,6 +558,7 @@
      * Creates the result Intent for the given contactLookupUri that should started after a
      * successful saving a contact.
      */
+    // TODO: move to ContactEditorUtils?
     protected static Intent composeQuickContactsIntent(Context context, Uri contactLookupUri) {
         final Intent intent = QuickContact.composeQuickContactsIntent(
                 context, (Rect) null, contactLookupUri, QuickContactActivity.MODE_FULLY_EXPANDED,
diff --git a/src/com/android/contacts/editor/ContactEditorFragment.java b/src/com/android/contacts/editor/ContactEditorFragment.java
index d9db85c..6639e1b 100644
--- a/src/com/android/contacts/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorFragment.java
@@ -16,33 +16,25 @@
 
 package com.android.contacts.editor;
 
-import android.accounts.Account;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.app.DialogFragment;
-import android.app.LoaderManager;
-import android.app.LoaderManager.LoaderCallbacks;
 import android.content.ActivityNotFoundException;
 import android.content.ContentUris;
 import android.content.Context;
-import android.content.CursorLoader;
 import android.content.DialogInterface;
 import android.content.Intent;
-import android.content.Loader;
-import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.media.RingtoneManager;
 import android.net.Uri;
 import android.os.Bundle;
-import android.os.SystemClock;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.Event;
 import android.provider.ContactsContract.CommonDataKinds.Organization;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.Photo;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
-import android.provider.ContactsContract.Groups;
 import android.provider.ContactsContract.Intents;
 import android.provider.ContactsContract.RawContacts;
 import android.text.TextUtils;
@@ -61,14 +53,11 @@
 import android.widget.Toast;
 
 import com.android.contacts.ContactSaveService;
-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.ContactEditor;
 import com.android.contacts.common.model.AccountTypeManager;
 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.RawContactDelta;
 import com.android.contacts.common.model.RawContactDeltaList;
@@ -103,27 +92,6 @@
         AggregationSuggestionEngine.Listener, AggregationSuggestionView.Listener,
         RawContactReadOnlyEditorView.Listener {
 
-    private static final int LOADER_DATA = 1;
-    private static final int LOADER_GROUPS = 2;
-
-    private static final String KEY_ACTION = "action";
-    private static final String KEY_URI = "uri";
-    private static final String KEY_DISABLE_DELETE_MENU_OPTION = "disableDeleteMenuOption";
-    private static final String KEY_NEW_LOCAL_PROFILE = "newLocalProfile";
-
-    private static final String KEY_VIEW_ID_GENERATOR = "viewidgenerator";
-
-    private static final String KEY_RAW_CONTACTS = "rawContacts";
-
-    private static final String KEY_EDIT_STATE = "state";
-    private static final String KEY_STATUS = "status";
-
-    private static final String KEY_HAS_NEW_CONTACT = "hasNewContact";
-    private static final String KEY_NEW_CONTACT_READY = "newContactDataReady";
-
-    private static final String KEY_IS_EDIT = "isEdit";
-    private static final String KEY_EXISTING_CONTACT_READY = "existingContactDataReady";
-
     // Phone option menus
     private static final String KEY_SEND_TO_VOICE_MAIL_STATE = "sendToVoicemailState";
     private static final String KEY_ARE_PHONE_OPTIONS_CHANGEABLE = "arePhoneOptionsChangable";
@@ -161,54 +129,14 @@
     public static final String INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION =
             "disableDeleteMenuOption";
 
-    private static final int REQUEST_CODE_JOIN = 0;
-    private static final int REQUEST_CODE_ACCOUNTS_CHANGED = 1;
-    private static final int REQUEST_CODE_PICK_RINGTONE = 2;
-
-    private Context mContext;
-    private Listener mListener;
-
-    private LinearLayout mContent;
-
-    //
-    // Parameters passed in on {@link #load}
-    //
-    private String mAction;
-    private Uri mLookupUri;
-    private Bundle mIntentExtras;
-    private boolean mAutoAddToDefaultGroup;
-    private boolean mDisableDeleteMenuOption = false;
-    private boolean mNewLocalProfile = false;
-
     //
     // Helpers
     //
-    private ContactEditorUtils mEditorUtils;
-    private RawContactDeltaComparator mComparator;
-    private ViewIdGenerator mViewIdGenerator;
     private AggregationSuggestionEngine mAggregationSuggestionEngine;
 
     //
-    // Loaded data
-    //
-    // Used to temporarily store existing contact data during a rebind call (i.e. account switch)
-    private ImmutableList<RawContact> mRawContacts;
-    private Cursor mGroupMetaData;
-
-    //
     // Contact editor state
     //
-    private RawContactDeltaList mState;
-    private int mStatus;
-
-    // Whether to show the new contact blank form and if it's corresponding delta is ready.
-    private boolean mHasNewContact = false;
-    private boolean mNewContactDataReady = false;
-
-    // Whether it's an edit of existing contact and if it's corresponding delta is ready.
-    private boolean mIsEdit = false;
-    private boolean mExistingContactDataReady = false;
-
     // Phone specific option menus
     private boolean mSendToVoicemailState;
     private boolean mArePhoneOptionsChangable;
@@ -330,14 +258,6 @@
     }
 
     @Override
-    public void onAttach(Activity activity) {
-        super.onAttach(activity);
-        mContext = activity;
-        mEditorUtils = ContactEditorUtils.getInstance(mContext);
-        mComparator = new RawContactDeltaComparator(mContext);
-    }
-
-    @Override
     public void onStop() {
         super.onStop();
 
@@ -369,67 +289,6 @@
     }
 
     @Override
-    public void onActivityCreated(Bundle savedInstanceState) {
-        super.onActivityCreated(savedInstanceState);
-
-        validateAction(mAction);
-
-        if (mState.isEmpty()) {
-            // The delta list may not have finished loading before orientation change happens.
-            // In this case, there will be a saved state but deltas will be missing.  Reload from
-            // database.
-            if (Intent.ACTION_EDIT.equals(mAction)) {
-                // Either...
-                // 1) orientation change but load never finished.
-                // or
-                // 2) not an orientation change.  data needs to be loaded for first time.
-                getLoaderManager().initLoader(LOADER_DATA, null, mDataLoaderListener);
-            }
-        } else {
-            // Orientation change, we already have mState, it was loaded by onCreate
-            bindEditors();
-        }
-
-        // Handle initial actions only when existing state missing
-        if (savedInstanceState == null) {
-            if (Intent.ACTION_EDIT.equals(mAction)) {
-                mIsEdit = true;
-            } else if (Intent.ACTION_INSERT.equals(mAction)) {
-                mHasNewContact = true;
-                final Account account = mIntentExtras == null ? null :
-                        (Account) mIntentExtras.getParcelable(Intents.Insert.EXTRA_ACCOUNT);
-                final String dataSet = mIntentExtras == null ? null :
-                        mIntentExtras.getString(Intents.Insert.EXTRA_DATA_SET);
-
-                if (account != null) {
-                    // Account specified in Intent
-                    createContact(new AccountWithDataSet(account.name, account.type, dataSet));
-                } else {
-                    // No Account specified. Let the user choose
-                    // Load Accounts async so that we can present them
-                    selectAccountAndCreateContact();
-                }
-            }
-        }
-    }
-
-    /**
-     * Checks if the requested action is valid.
-     *
-     * @param action The action to test.
-     * @throws IllegalArgumentException when the action is invalid.
-     */
-    private void validateAction(String action) {
-        if (Intent.ACTION_EDIT.equals(action) || Intent.ACTION_INSERT.equals(action) ||
-                ContactEditorActivity.ACTION_SAVE_COMPLETED.equals(action)) {
-            return;
-        }
-        throw new IllegalArgumentException("Unknown Action String " + mAction +
-                ". Only support " + Intent.ACTION_EDIT + " or " + Intent.ACTION_INSERT + " or " +
-                ContactEditorActivity.ACTION_SAVE_COMPLETED);
-    }
-
-    @Override
     public void onStart() {
         getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupLoaderListener);
         super.onStart();
@@ -455,42 +314,13 @@
 
     @Override
     public void onCreate(Bundle savedState) {
-        if (savedState != null) {
-            // Restore mUri before calling super.onCreate so that onInitializeLoaders
-            // would already have a uri and an action to work with
-            mAction = savedState.getString(KEY_ACTION);
-            mLookupUri = savedState.getParcelable(KEY_URI);
-        }
-
         super.onCreate(savedState);
 
-        if (savedState == null) {
-            // If savedState is non-null, onRestoreInstanceState() will restore the generator.
-            mViewIdGenerator = new ViewIdGenerator();
-        } else {
-            mViewIdGenerator = savedState.getParcelable(KEY_VIEW_ID_GENERATOR);
-
-            mDisableDeleteMenuOption = savedState.getBoolean(KEY_DISABLE_DELETE_MENU_OPTION);
-            mNewLocalProfile = savedState.getBoolean(KEY_NEW_LOCAL_PROFILE);
-
-            mRawContacts = ImmutableList.copyOf(savedState.<RawContact>getParcelableArrayList(
-                    KEY_RAW_CONTACTS));
-            // NOTE: mGroupMetaData is not saved/restored
-
-            // Read state from savedState. No loading involved here
-            mState = savedState.<RawContactDeltaList> getParcelable(KEY_EDIT_STATE);
-            mStatus = savedState.getInt(KEY_STATUS);
-
-            mHasNewContact = savedState.getBoolean(KEY_HAS_NEW_CONTACT);
-            mNewContactDataReady = savedState.getBoolean(KEY_NEW_CONTACT_READY);
-
-            mIsEdit = savedState.getBoolean(KEY_IS_EDIT);
-            mExistingContactDataReady = savedState.getBoolean(KEY_EXISTING_CONTACT_READY);
-
+        if (savedState != null) {
             // Phone specific options menus
             mSendToVoicemailState = savedState.getBoolean(KEY_SEND_TO_VOICE_MAIL_STATE);
             mArePhoneOptionsChangable = savedState.getBoolean(KEY_ARE_PHONE_OPTIONS_CHANGEABLE);
-            mCustomRingtone =  savedState.getString(KEY_CUSTOM_RINGTONE);
+            mCustomRingtone = savedState.getString(KEY_CUSTOM_RINGTONE);
 
             // Joins
             mContactIdForJoin = savedState.getLong(KEY_CONTACT_ID_FOR_JOIN);
@@ -515,60 +345,6 @@
             mAggregationSuggestionsRawContactId = savedState.getLong(
                     KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID);
         }
-
-        // mState can still be null because it may not have have finished loading before
-        // onSaveInstanceState was called.
-        if (mState == null) {
-            mState = new RawContactDeltaList();
-        }
-    }
-
-    public void setData(Contact contact) {
-
-        // If we have already loaded data, we do not want to change it here to not confuse the user
-        if (!mState.isEmpty()) {
-            Log.v(TAG, "Ignoring background change. This will have to be rebased later");
-            return;
-        }
-
-        // See if this edit operation needs to be redirected to a custom editor
-        mRawContacts = contact.getRawContacts();
-        if (mRawContacts.size() == 1) {
-            RawContact rawContact = mRawContacts.get(0);
-            String type = rawContact.getAccountTypeString();
-            String dataSet = rawContact.getDataSet();
-            AccountType accountType = rawContact.getAccountType(mContext);
-            if (accountType.getEditContactActivityClassName() != null &&
-                    !accountType.areContactsWritable()) {
-                if (mListener != null) {
-                    String name = rawContact.getAccountName();
-                    long rawContactId = rawContact.getId();
-                    mListener.onCustomEditContactActivityRequested(
-                            new AccountWithDataSet(name, type, dataSet),
-                            ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
-                            mIntentExtras, true);
-                }
-                return;
-            }
-        }
-
-        String displayName = null;
-        // Check for writable raw contacts.  If there are none, then we need to create one so user
-        // can edit.  For the user profile case, there is already an editable contact.
-        if (!contact.isUserProfile() && !contact.isWritableContact(mContext)) {
-            mHasNewContact = true;
-
-            // This is potentially an asynchronous call and will add deltas to list.
-            selectAccountAndCreateContact();
-            displayName = contact.getDisplayName();
-        }
-
-        // This also adds deltas to list
-        // If displayName is null at this point it is simply ignored later on by the editor.
-        bindEditorsForExistingContact(displayName, contact.isUserProfile(),
-                mRawContacts);
-
-        bindMenuItemsForPhone(contact);
     }
 
     @Override
@@ -581,7 +357,8 @@
         updatedExpandedEditorsMap();
     }
 
-    private void bindEditorsForExistingContact(String displayName, boolean isUserProfile,
+    @Override
+    protected void bindEditorsForExistingContact(String displayName, boolean isUserProfile,
             ImmutableList<RawContact> rawContacts) {
         setEnabled(true);
         mDefaultDisplayName = displayName;
@@ -619,7 +396,8 @@
         bindEditors();
     }
 
-    private void bindMenuItemsForPhone(Contact contact) {
+    @Override
+    protected void bindMenuItemsForPhone(Contact contact) {
         mSendToVoicemailState = contact.isSendToVoicemail();
         mCustomRingtone = contact.getCustomRingtone();
         mArePhoneOptionsChangable = arePhoneOptionsChangable(contact);
@@ -649,64 +427,6 @@
         }
     }
 
-    private void selectAccountAndCreateContact() {
-        // If this is a local profile, then skip the logic about showing the accounts changed
-        // activity and create a phone-local contact.
-        if (mNewLocalProfile) {
-            createContact(null);
-            return;
-        }
-
-        // If there is no default account or the accounts have changed such that we need to
-        // prompt the user again, then launch the account prompt.
-        if (mEditorUtils.shouldShowAccountChangedNotification()) {
-            Intent intent = new Intent(mContext, ContactEditorAccountsChangedActivity.class);
-            mStatus = Status.SUB_ACTIVITY;
-            startActivityForResult(intent, REQUEST_CODE_ACCOUNTS_CHANGED);
-        } else {
-            // Otherwise, there should be a default account. Then either create a local contact
-            // (if default account is null) or create a contact with the specified account.
-            AccountWithDataSet defaultAccount = mEditorUtils.getDefaultAccount();
-            createContact(defaultAccount);
-        }
-    }
-
-    /**
-     * Create a contact by automatically selecting the first account. If there's no available
-     * account, a device-local contact should be created.
-     */
-    private void createContact() {
-        final List<AccountWithDataSet> accounts =
-                AccountTypeManager.getInstance(mContext).getAccounts(true);
-        // No Accounts available. Create a phone-local contact.
-        if (accounts.isEmpty()) {
-            createContact(null);
-            return;
-        }
-
-        // We have an account switcher in "create-account" screen, so don't need to ask a user to
-        // select an account here.
-        createContact(accounts.get(0));
-    }
-
-    /**
-     * Shows account creation screen associated with a given account.
-     *
-     * @param account may be null to signal a device-local contact should be created.
-     */
-    private void createContact(AccountWithDataSet account) {
-        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
-        final AccountType accountType = accountTypes.getAccountTypeForAccount(account);
-
-        if (accountType.getCreateContactActivityClassName() != null) {
-            if (mListener != null) {
-                mListener.onCustomCreateContactActivityRequested(account, mIntentExtras);
-            }
-        } else {
-            bindEditorsForNewContact(account, accountType);
-        }
-    }
-
     /**
      * Removes a current editor ({@link #mState}) and rebinds new editor for a new account.
      * Some of old data are reused with new restriction enforced by the new account.
@@ -738,7 +458,8 @@
         }
     }
 
-    private void bindEditorsForNewContact(AccountWithDataSet account,
+    @Override
+    protected void bindEditorsForNewContact(AccountWithDataSet account,
             final AccountType accountType) {
         bindEditorsForNewContact(account, accountType, null, null);
     }
@@ -783,7 +504,8 @@
         bindEditors();
     }
 
-    private void bindEditors() {
+    @Override
+    protected void bindEditors() {
         // bindEditors() can only bind views if there is data in mState, so immediately return
         // if mState is null
         if (mState.isEmpty()) {
@@ -989,7 +711,8 @@
         }
     }
 
-    private void bindGroupMetaData() {
+    @Override
+    protected void bindGroupMetaData() {
         if (mGroupMetaData == null) {
             return;
         }
@@ -1178,14 +901,6 @@
     }
 
     /**
-     * Check if our internal {@link #mState} is valid, usually checked before
-     * performing user actions.
-     */
-    private boolean hasValidState() {
-        return mState.size() > 0;
-    }
-
-    /**
      * Return true if there are any edits to the current contact which need to
      * be saved.
      */
@@ -1318,14 +1033,12 @@
         if (mListener != null) mListener.onReverted();
     }
 
-    public void doSaveAction() {
-        save(SaveMode.CLOSE);
-    }
-
+    @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) {
@@ -1619,28 +1332,6 @@
 
     @Override
     public void onSaveInstanceState(Bundle outState) {
-        outState.putString(KEY_ACTION, mAction);
-        outState.putParcelable(KEY_URI, mLookupUri);
-        outState.putBoolean(KEY_DISABLE_DELETE_MENU_OPTION, mDisableDeleteMenuOption);
-        outState.putBoolean(KEY_NEW_LOCAL_PROFILE, mNewLocalProfile);
-
-        outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator);
-
-        outState.putParcelableArrayList(KEY_RAW_CONTACTS, mRawContacts == null
-                ? Lists.<RawContact>newArrayList() : Lists.newArrayList(mRawContacts));
-
-        if (hasValidState()) {
-            // Store entities with modifications
-            outState.putParcelable(KEY_EDIT_STATE, mState);
-        }
-        outState.putInt(KEY_STATUS, mStatus);
-
-        outState.putBoolean(KEY_HAS_NEW_CONTACT, mHasNewContact);
-        outState.putBoolean(KEY_NEW_CONTACT_READY, mNewContactDataReady);
-
-        outState.putBoolean(KEY_IS_EDIT, mIsEdit);
-        outState.putBoolean(KEY_EXISTING_CONTACT_READY, mExistingContactDataReady);
-
         // Phone specific options
         outState.putBoolean(KEY_SEND_TO_VOICE_MAIL_STATE, mSendToVoicemailState);
         outState.putBoolean(KEY_ARE_PHONE_OPTIONS_CHANGEABLE, mArePhoneOptionsChangable);
@@ -1651,9 +1342,7 @@
         outState.putBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN, mContactWritableForJoin);
 
         outState.putSerializable(KEY_EXPANDED_EDITORS, mExpandedEditors);
-
         outState.putBoolean(KEY_IS_USER_PROFILE, mIsUserProfile);
-
         outState.putBoolean(KEY_ENABLED, mEnabled);
 
         // Photos
@@ -1801,68 +1490,6 @@
         return false;
     }
 
-    /**
-     * The listener for the data loader
-     */
-    private final LoaderManager.LoaderCallbacks<Contact> mDataLoaderListener =
-            new LoaderCallbacks<Contact>() {
-
-        private long mLoaderStartTime;
-
-        @Override
-        public Loader<Contact> onCreateLoader(int id, Bundle args) {
-            mLoaderStartTime = SystemClock.elapsedRealtime();
-            return new ContactLoader(mContext, mLookupUri, true);
-        }
-
-        @Override
-        public void onLoadFinished(Loader<Contact> loader, Contact data) {
-            final long loaderCurrentTime = SystemClock.elapsedRealtime();
-            Log.v(TAG, "Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime));
-            if (!data.isLoaded()) {
-                // Item has been deleted. Close activity without saving again.
-                Log.i(TAG, "No contact found. Closing activity");
-                mStatus = Status.CLOSING;
-                if (mListener != null) mListener.onContactNotFound();
-                return;
-            }
-
-            mStatus = Status.EDITING;
-            mLookupUri = data.getLookupUri();
-            final long setDataStartTime = SystemClock.elapsedRealtime();
-            setData(data);
-            final long setDataEndTime = SystemClock.elapsedRealtime();
-
-            Log.v(TAG, "Time needed for setting UI: " + (setDataEndTime-setDataStartTime));
-        }
-
-        @Override
-        public void onLoaderReset(Loader<Contact> loader) {
-        }
-    };
-
-    /**
-     * The listener for the group meta data loader for all groups.
-     */
-    private final LoaderManager.LoaderCallbacks<Cursor> mGroupLoaderListener =
-            new LoaderCallbacks<Cursor>() {
-
-        @Override
-        public CursorLoader onCreateLoader(int id, Bundle args) {
-            return new GroupMetaDataLoader(mContext, Groups.CONTENT_URI);
-        }
-
-        @Override
-        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
-            mGroupMetaData = data;
-            bindGroupMetaData();
-        }
-
-        @Override
-        public void onLoaderReset(Loader<Cursor> loader) {
-        }
-    };
-
     @Override
     public void onSplitContactConfirmed() {
         if (mState.isEmpty()) {