Merge "Making DefaultContactBrowserListFragment auto-configured"
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 05797f3..10a0645 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -386,7 +386,7 @@
 
         <!-- Edit or insert details for a contact -->
         <activity
-            android:name=".ui.EditContactActivity"
+            android:name=".activities.ContactEditActivity"
             android:windowSoftInputMode="stateHidden|adjustResize">
 
             <intent-filter android:label="@string/editContactDescription">
diff --git a/res/layout/contact_detail.xml b/res/layout/contact_detail.xml
index b4f8101..10d3e48 100644
--- a/res/layout/contact_detail.xml
+++ b/res/layout/contact_detail.xml
@@ -15,7 +15,7 @@
 -->
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/contact_details"
+    android:id="@+id/contact_detail"
     android:orientation="vertical"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
diff --git a/res/layout/contact_edit.xml b/res/layout/contact_edit.xml
new file mode 100644
index 0000000..25dff3f
--- /dev/null
+++ b/res/layout/contact_edit.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/contact_edit"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+>
+
+    <ScrollView
+        android:layout_width="match_parent"
+        android:layout_height="1px"
+        android:layout_weight="1"
+        android:fillViewport="true"
+    >
+
+        <LinearLayout android:id="@+id/editors"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:orientation="vertical"
+        />
+
+    </ScrollView>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        style="@android:style/ButtonBar"
+    >
+
+        <Button android:id="@+id/btn_done"
+            android:layout_width="0dip"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:text="@string/menu_done"
+        />
+
+        <Button android:id="@+id/btn_discard"
+            android:layout_width="0dip"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:text="@string/menu_doNotSave"
+        />
+
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/res/values/ids.xml b/res/values/ids.xml
index 692c413..83a0cf3 100644
--- a/res/values/ids.xml
+++ b/res/values/ids.xml
@@ -14,12 +14,29 @@
      limitations under the License.
 -->
 <resources>
-    <!-- The EditText for entries in the EditContactActivity -->
+    <!-- Dialogs in ContactDetailFragment -->
+    <item type="id" name="detail_dialog_confirm_delete" />
+    <item type="id" name="detail_dialog_confirm_readonly_delete" />
+    <item type="id" name="detail_dialog_confirm_multiple_delete" />
+    <item type="id" name="detail_dialog_confirm_readonly_hide" />
+
+    <!-- The EditText for entries in the ContactEditFragment -->
     <item type="id" name="data"/>
     <item type="id" name="header_phones"/>
     <item type="id" name="dialog_sync_add"/>
     <item type="id" name="dialog_import_export"/>
 
+    <!-- Dialogs in ContactEditFragment -->
+    <item type="id" name="edit_dialog_confirm_delete"/>
+    <item type="id" name="edit_dialog_confirm_readonly_delete"/>
+    <item type="id" name="edit_dialog_confirm_multiple_delete"/>
+    <item type="id" name="edit_dialog_confirm_readonly_hide"/>
+    <item type="id" name="edit_dialog_pick_photo"/>
+    <item type="id" name="edit_dialog_split"/>
+    <item type="id" name="edit_dialog_select_account"/>
+    <item type="id" name="edit_dialog_view_dialogs_id1"/>
+    <item type="id" name="edit_dialog_view_dialogs_id2"/>
+
     <!-- For ImportVCardActivity -->
     <item type="id" name="dialog_searching_vcard"/>
     <item type="id" name="dialog_sdcard_not_found"/>
diff --git a/src/com/android/contacts/activities/ContactDetailActivity.java b/src/com/android/contacts/activities/ContactDetailActivity.java
index d413a25..a843141 100644
--- a/src/com/android/contacts/activities/ContactDetailActivity.java
+++ b/src/com/android/contacts/activities/ContactDetailActivity.java
@@ -18,13 +18,11 @@
 
 import com.android.contacts.ContactsSearchManager;
 import com.android.contacts.R;
-import com.android.contacts.util.DialogManager;
-import com.android.contacts.views.detail.ContactPresenter;
-import com.android.contacts.views.detail.ContactLoader;
+import com.android.contacts.views.detail.ContactDetailFragment;
 
+import android.app.Activity;
 import android.app.Dialog;
-import android.app.patterns.Loader;
-import android.app.patterns.LoaderActivity;
+import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
 import android.util.Log;
@@ -32,16 +30,12 @@
 import android.view.Menu;
 import android.view.MenuItem;
 
-public class ContactDetailActivity extends LoaderActivity<ContactLoader.Result> implements
-        DialogManager.DialogShowingViewActivity {
-    private static final int LOADER_DETAILS = 1;
-    private ContactPresenter mCoupler;
-    private DialogManager mDialogManager;
+public class ContactDetailActivity extends Activity {
+    private ContactDetailFragment mFragment;
 
     private static final String TAG = "ContactDetailActivity";
 
-    private static final int DIALOG_VIEW_DIALOGS_ID1 = 1;
-    private static final int DIALOG_VIEW_DIALOGS_ID2 = 2;
+    private final FragmentCallbackHandler mCallbackHandler = new FragmentCallbackHandler();
 
     @Override
     public void onCreate(Bundle savedState) {
@@ -49,53 +43,18 @@
 
         setContentView(R.layout.contact_detail);
 
-        mDialogManager = new DialogManager(this, DIALOG_VIEW_DIALOGS_ID1, DIALOG_VIEW_DIALOGS_ID2);
+        mFragment = new ContactDetailFragment(this, findViewById(R.id.contact_detail),
+                mCallbackHandler, getIntent().getData());
 
-        mCoupler = new ContactPresenter(this, findViewById(R.id.contact_details));
-        mCoupler.setController(new ContactPresenter.DefaultController(this));
-    }
-
-    @Override
-    public void onInitializeLoaders() {
-        startLoading(LOADER_DETAILS, null);
-    }
-
-    @Override
-    protected ContactLoader onCreateLoader(int id, Bundle args) {
-        switch (id) {
-            case LOADER_DETAILS: {
-                return new ContactLoader(this, getIntent().getData());
-            }
-            default: {
-                Log.wtf(TAG, "Unknown ID in onCreateLoader: " + id);
-            }
-        }
-        return null;
-    }
-
-    @Override
-    public void onLoadFinished(Loader loader, ContactLoader.Result data) {
-        final int id = loader.getId();
-        switch (id) {
-            case LOADER_DETAILS:
-                if (data == ContactLoader.Result.NOT_FOUND) {
-                    // Item has been deleted
-                    Log.i(TAG, "No contact found. Closing activity");
-                    finish();
-                    return;
-                }
-                mCoupler.setData(data);
-                break;
-            default: {
-                Log.wtf(TAG, "Unknown ID in onLoadFinished: " + id);
-            }
-        }
+        openFragmentTransaction()
+            .add(mFragment, R.id.contact_detail)
+            .commit();
     }
 
     @Override
     public boolean onCreateOptionsMenu(Menu menu) {
         // TODO: This is too hardwired.
-        if (mCoupler.onCreateOptionsMenu(menu, getMenuInflater())) return true;
+        if (mFragment.onCreateOptionsMenu(menu, getMenuInflater())) return true;
 
         return super.onCreateOptionsMenu(menu);
     }
@@ -103,7 +62,7 @@
     @Override
     public boolean onPrepareOptionsMenu(Menu menu) {
         // TODO: This is too hardwired.
-        if (mCoupler.onPrepareOptionsMenu(menu)) return true;
+        if (mFragment.onPrepareOptionsMenu(menu)) return true;
 
         return super.onPrepareOptionsMenu(menu);
     }
@@ -111,24 +70,26 @@
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         // TODO: This is too hardwired.
-        if (mCoupler.onOptionsItemSelected(item)) return true;
+        if (mFragment.onOptionsItemSelected(item)) return true;
 
         return super.onOptionsItemSelected(item);
     }
 
-    public DialogManager getDialogManager() {
-        return mDialogManager;
-    }
-
     @Override
     protected Dialog onCreateDialog(int id, Bundle args) {
-        return mDialogManager.onCreateDialog(id, args);
+        // ask the Fragment whether it knows about the dialog
+        final Dialog fragmentResult = mFragment.onCreateDialog(id, args);
+        if (fragmentResult != null) return fragmentResult;
+
+        // Nobody knows about the Dialog
+        Log.w(TAG, "Unknown dialog requested, id: " + id + ", args: " + args);
+        return null;
     }
 
     @Override
     public boolean onContextItemSelected(MenuItem item) {
         // TODO: This is too hardwired.
-        if (mCoupler.onContextItemSelected(item)) return true;
+        if (mFragment.onContextItemSelected(item)) return true;
 
         return super.onContextItemSelected(item);
     }
@@ -146,8 +107,22 @@
     @Override
     public boolean onKeyDown(int keyCode, KeyEvent event) {
         // TODO: This is too hardwired.
-        if (mCoupler.onKeyDown(keyCode, event)) return true;
+        if (mFragment.onKeyDown(keyCode, event)) return true;
 
         return super.onKeyDown(keyCode, event);
     }
+
+    private class FragmentCallbackHandler implements ContactDetailFragment.Callbacks {
+        public void closeBecauseContactNotFound() {
+            finish();
+        }
+
+        public void editContact(Uri rawContactUri) {
+            startActivity(new Intent(Intent.ACTION_EDIT, rawContactUri));
+        }
+
+        public void itemClicked(Intent intent) {
+            startActivity(intent);
+        }
+    }
 }
diff --git a/src/com/android/contacts/activities/ContactEditActivity.java b/src/com/android/contacts/activities/ContactEditActivity.java
new file mode 100644
index 0000000..cf1fbac
--- /dev/null
+++ b/src/com/android/contacts/activities/ContactEditActivity.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.activities;
+
+import com.android.contacts.R;
+import com.android.contacts.util.DialogManager;
+import com.android.contacts.views.edit.ContactEditFragment;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+public class ContactEditActivity extends Activity implements
+        DialogManager.DialogShowingViewActivity {
+
+    private static final String TAG = "ContactEditActivity";
+    private static final int DIALOG_VIEW_DIALOGS_ID1 = 1;
+    private static final int DIALOG_VIEW_DIALOGS_ID2 = 2;
+
+    private final FragmentCallbackHandler mCallbackHandler = new FragmentCallbackHandler();
+    private final DialogManager mDialogManager = new DialogManager(this, DIALOG_VIEW_DIALOGS_ID1,
+            DIALOG_VIEW_DIALOGS_ID2);
+
+    private ContactEditFragment mFragment;
+
+    @Override
+    public void onCreate(Bundle savedState) {
+        super.onCreate(savedState);
+
+        setContentView(R.layout.contact_edit);
+
+        final Intent intent = getIntent();
+        final String action = intent.getAction();
+        final Uri uri = intent.getData();
+        final String mimeType = intent.resolveType(getContentResolver());
+        final Bundle intentExtras = intent.getExtras();
+
+        mFragment = new ContactEditFragment(
+                this, findViewById(R.id.contact_edit),
+                action, uri, mimeType, intentExtras,
+                mCallbackHandler);
+
+        openFragmentTransaction()
+            .add(mFragment, R.id.contact_edit)
+            .commit();
+    }
+
+    private class FragmentCallbackHandler implements ContactEditFragment.Callbacks {
+        public void closeAfterRevert() {
+            finish();
+        }
+
+        public void closeBecauseContactNotFound() {
+            finish();
+        }
+
+        public void setTitleTo(int resourceId) {
+            setTitle(resourceId);
+        }
+
+        public void closeAfterSaving(int resultCode, Intent resultIntent) {
+            setResult(resultCode, resultIntent);
+            finish();
+        }
+    }
+
+    public DialogManager getDialogManager() {
+        return mDialogManager;
+    }
+
+    @Override
+    protected Dialog onCreateDialog(int id, Bundle args) {
+        // If this is a dynamic dialog, use the DialogManager
+        if (id == DIALOG_VIEW_DIALOGS_ID1 || id == DIALOG_VIEW_DIALOGS_ID2) {
+            final Dialog dialog = mDialogManager.onCreateDialog(id, args);
+            if (dialog != null) return dialog;
+            return super.onCreateDialog(id, args);
+        }
+
+        // ask the Fragment whether it knows about the dialog
+        final Dialog fragmentResult = mFragment.onCreateDialog(id, args);
+        if (fragmentResult != null) return fragmentResult;
+
+        // Nobody knows about the Dialog
+        Log.w(TAG, "Unknown dialog requested, id: " + id + ", args: " + args);
+        return null;
+    }
+}
diff --git a/src/com/android/contacts/list/ContactBrowseListContextMenuAdapter.java b/src/com/android/contacts/list/ContactBrowseListContextMenuAdapter.java
index c9d4635..617c855 100644
--- a/src/com/android/contacts/list/ContactBrowseListContextMenuAdapter.java
+++ b/src/com/android/contacts/list/ContactBrowseListContextMenuAdapter.java
@@ -56,7 +56,8 @@
         }
 
         ContactListAdapter adapter = mContactListFragment.getAdapter();
-        adapter.moveToPosition(info.position);
+        int headerViewsCount = mContactListFragment.getListView().getHeaderViewsCount();
+        adapter.moveToPosition(info.position - headerViewsCount);
 
         // Setup the menu header
         menu.setHeaderTitle(adapter.getContactDisplayName());
@@ -93,7 +94,8 @@
         }
 
         ContactListAdapter adapter = mContactListFragment.getAdapter();
-        adapter.moveToPosition(info.position);
+        int headerViewsCount = mContactListFragment.getListView().getHeaderViewsCount();
+        adapter.moveToPosition(info.position - headerViewsCount);
 
         final Uri contactUri = adapter.getContactUri();
         switch (item.getItemId()) {
diff --git a/src/com/android/contacts/views/detail/ContactPresenter.java b/src/com/android/contacts/views/detail/ContactDetailFragment.java
similarity index 90%
rename from src/com/android/contacts/views/detail/ContactPresenter.java
rename to src/com/android/contacts/views/detail/ContactDetailFragment.java
index d2dfda1..fb781a9 100644
--- a/src/com/android/contacts/views/detail/ContactPresenter.java
+++ b/src/com/android/contacts/views/detail/ContactDetailFragment.java
@@ -27,15 +27,15 @@
 import com.android.contacts.model.ContactsSource;
 import com.android.contacts.model.Sources;
 import com.android.contacts.model.ContactsSource.DataKind;
-import com.android.contacts.util.DialogManager;
 import com.android.contacts.util.Constants;
 import com.android.contacts.util.DataStatus;
-import com.android.contacts.views.detail.ContactLoader.Result;
 import com.android.internal.telephony.ITelephony;
 import com.android.internal.widget.ContactHeaderWidget;
 
 import android.app.AlertDialog;
 import android.app.Dialog;
+import android.app.patterns.Loader;
+import android.app.patterns.LoaderManagingFragment;
 import android.content.ActivityNotFoundException;
 import android.content.ContentUris;
 import android.content.ContentValues;
@@ -89,36 +89,32 @@
 
 import java.util.ArrayList;
 
-public class ContactPresenter implements OnCreateContextMenuListener,
-        OnItemClickListener, DialogManager.DialogShowingView {
+public class ContactDetailFragment extends LoaderManagingFragment<ContactDetailLoader.Result>
+        implements OnCreateContextMenuListener, OnItemClickListener {
     private static final String TAG = "ContactDetailsView";
     private static final boolean SHOW_SEPARATORS = false;
 
-    private static final String DIALOG_ID_KEY = "dialog_id";
-    private static final int DIALOG_CONFIRM_DELETE = 1;
-    private static final int DIALOG_CONFIRM_READONLY_DELETE = 2;
-    private static final int DIALOG_CONFIRM_MULTIPLE_DELETE = 3;
-    private static final int DIALOG_CONFIRM_READONLY_HIDE = 4;
-
     private static final int MENU_ITEM_MAKE_DEFAULT = 3;
 
+    private static final int LOADER_DETAILS = 1;
+
     private final Context mContext;
     private final View mView;
+    private final LayoutInflater mInflater;
+    private final Uri mLookupUri;
+    private Callbacks mCallbacks;
 
-    private Result mContactData;
-    private Controller mCallbacks;
-    private LayoutInflater mInflater;
+    private ContactDetailLoader.Result mContactData;
     private ContactHeaderWidget mContactHeaderWidget;
     private ListView mListView;
     private boolean mShowSmsLinksForAllPhones;
     private ViewAdapter mAdapter;
     private Uri mPrimaryPhoneUri = null;
-    private DialogManager mDialogManager = null;
 
     private int mReadOnlySourcesCnt;
     private int mWritableSourcesCnt;
     private boolean mAllRestricted;
-    private ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>();
+    private final ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>();
     private int mNumPhoneNumbers = 0;
 
     /**
@@ -143,9 +139,12 @@
     private ArrayList<ViewEntry> mOtherEntries = new ArrayList<ViewEntry>();
     private ArrayList<ArrayList<ViewEntry>> mSections = new ArrayList<ArrayList<ViewEntry>>();
 
-    public ContactPresenter(Context context, View view) {
+    public ContactDetailFragment(Context context, View view, Callbacks callbacks, Uri lookupUri) {
+        super();
+        if (callbacks == null) throw new IllegalArgumentException("callbacks must be provided");
         mContext = context;
         mView = view;
+        mCallbacks = callbacks;
 
         mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
         mContactHeaderWidget = (ContactHeaderWidget) view.findViewById(R.id.contact_header_widget);
@@ -175,12 +174,47 @@
 
         //TODO Read this value from a preference
         mShowSmsLinksForAllPhones = true;
+
+        mLookupUri = lookupUri;
     }
 
-    public void setData(Result data) {
-        mContactData = data;
+    @Override
+    protected void onInitializeLoaders() {
+        startLoading(LOADER_DETAILS, null);
+    }
 
-        bindData();
+    @Override
+    protected Loader<ContactDetailLoader.Result> onCreateLoader(int id, Bundle args) {
+        switch (id) {
+            case LOADER_DETAILS: {
+                return new ContactDetailLoader(mContext, mLookupUri);
+            }
+            default: {
+                Log.wtf(TAG, "Unknown ID in onCreateLoader: " + id);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    protected void onLoadFinished(Loader<ContactDetailLoader.Result> loader,
+            ContactDetailLoader.Result data) {
+        final int id = loader.getId();
+        switch (id) {
+            case LOADER_DETAILS:
+                if (data == ContactDetailLoader.Result.NOT_FOUND) {
+                    // Item has been deleted
+                    Log.i(TAG, "No contact found. Closing activity");
+                    mCallbacks.closeBecauseContactNotFound();
+                    return;
+                }
+                mContactData = data;
+                bindData();
+                break;
+            default: {
+                Log.wtf(TAG, "Unknown ID in onLoadFinished: " + id);
+            }
+        }
     }
 
     private void bindData() {
@@ -412,45 +446,6 @@
         }
     }
 
-    public interface Controller {
-        public void onPrimaryClick(ViewEntry entry);
-        public void onSecondaryClick(ViewEntry entry);
-    }
-
-    public static final class DefaultController implements Controller {
-        private Context mContext;
-
-        public DefaultController(Context context) {
-            mContext = context;
-        }
-
-        public void onPrimaryClick(ViewEntry entry) {
-            Intent intent = entry.intent;
-            if (intent != null) {
-                try {
-                    mContext.startActivity(intent);
-                } catch (ActivityNotFoundException e) {
-                    Log.e(TAG, "No activity found for intent: " + intent);
-                }
-            }
-        }
-
-        public void onSecondaryClick(ViewEntry entry) {
-            Intent intent = entry.secondaryIntent;
-            if (intent != null) {
-                try {
-                    mContext.startActivity(intent);
-                } catch (ActivityNotFoundException e) {
-                    Log.e(TAG, "No activity found for intent: " + intent);
-                }
-            }
-        }
-    }
-
-    public void setController(Controller callbacks) {
-        mCallbacks = callbacks;
-    }
-
     /* package */ static String buildActionString(DataKind kind, ContentValues values,
             boolean lowerCase, Context context) {
         if (kind.actionHeader == null) {
@@ -607,7 +602,6 @@
     }
 
     final class ViewAdapter extends ContactEntryAdapter<ViewEntry> implements OnClickListener {
-
         ViewAdapter(Context context, ArrayList<ArrayList<ViewEntry>> sections) {
             super(context, sections, SHOW_SEPARATORS);
         }
@@ -750,7 +744,9 @@
             if (v == null) return;
             final ViewEntry entry = (ViewEntry) v.getTag();
             if (entry == null) return;
-            mCallbacks.onSecondaryClick(entry);
+            final Intent intent = entry.secondaryIntent;
+            if (intent == null) return;
+            mCallbacks.itemClicked(intent);
         }
     }
 
@@ -774,10 +770,11 @@
         switch (item.getItemId()) {
             case R.id.menu_edit: {
                 if (mRawContactIds.size() > 0) {
-                    long rawContactIdToEdit = mRawContactIds.get(0);
-                    Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
+                    final long rawContactIdToEdit = mRawContactIds.get(0);
+                    final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
                             rawContactIdToEdit);
-                    mContext.startActivity(new Intent(Intent.ACTION_EDIT, rawContactUri));
+                    mCallbacks.editContact(rawContactUri);
+                    //mContext.startActivity(new Intent(Intent.ACTION_EDIT, rawContactUri));
                     return true;
                 } else {
                     // There is no rawContact to edit.
@@ -821,13 +818,13 @@
 
     private void showDeleteConfirmationDialog() {
         if (mReadOnlySourcesCnt > 0 & mWritableSourcesCnt > 0) {
-            showDialog(DIALOG_CONFIRM_READONLY_DELETE);
+            getActivity().showDialog(R.id.detail_dialog_confirm_readonly_delete);
         } else if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt == 0) {
-            showDialog(DIALOG_CONFIRM_READONLY_HIDE);
+            getActivity().showDialog(R.id.detail_dialog_confirm_readonly_hide);
         } else if (mReadOnlySourcesCnt == 0 && mWritableSourcesCnt > 1) {
-            showDialog(DIALOG_CONFIRM_MULTIPLE_DELETE);
+            getActivity().showDialog(R.id.detail_dialog_confirm_multiple_delete);
         } else {
-            showDialog(DIALOG_CONFIRM_DELETE);
+            getActivity().showDialog(R.id.detail_dialog_confirm_delete);
         }
     }
 
@@ -868,7 +865,9 @@
         if (mCallbacks == null) return;
         final ViewEntry entry = ViewAdapter.getEntry(mSections, position, SHOW_SEPARATORS);
         if (entry == null) return;
-        mCallbacks.onPrimaryClick(entry);
+        final Intent intent = entry.intent;
+        if (intent == null) return;
+        mCallbacks.itemClicked(intent);
     }
 
     private final DialogInterface.OnClickListener mDeleteListener =
@@ -878,11 +877,9 @@
         }
     };
 
-    public Dialog createDialog(Bundle bundle) {
-        if (bundle == null) throw new IllegalArgumentException("bundle must not be null");
-        int dialogId = bundle.getInt(DIALOG_ID_KEY);
-        switch (dialogId) {
-            case DIALOG_CONFIRM_DELETE:
+    public Dialog onCreateDialog(int id, Bundle bundle) {
+        switch (id) {
+            case R.id.detail_dialog_confirm_delete:
                 return new AlertDialog.Builder(mContext)
                         .setTitle(R.string.deleteConfirmation_title)
                         .setIcon(android.R.drawable.ic_dialog_alert)
@@ -891,7 +888,7 @@
                         .setPositiveButton(android.R.string.ok, mDeleteListener)
                         .setCancelable(false)
                         .create();
-            case DIALOG_CONFIRM_READONLY_DELETE:
+            case R.id.detail_dialog_confirm_readonly_delete:
                 return new AlertDialog.Builder(mContext)
                         .setTitle(R.string.deleteConfirmation_title)
                         .setIcon(android.R.drawable.ic_dialog_alert)
@@ -900,7 +897,7 @@
                         .setPositiveButton(android.R.string.ok, mDeleteListener)
                         .setCancelable(false)
                         .create();
-            case DIALOG_CONFIRM_MULTIPLE_DELETE:
+            case R.id.detail_dialog_confirm_multiple_delete:
                 return new AlertDialog.Builder(mContext)
                         .setTitle(R.string.deleteConfirmation_title)
                         .setIcon(android.R.drawable.ic_dialog_alert)
@@ -909,7 +906,7 @@
                         .setPositiveButton(android.R.string.ok, mDeleteListener)
                         .setCancelable(false)
                         .create();
-            case DIALOG_CONFIRM_READONLY_HIDE: {
+            case R.id.detail_dialog_confirm_readonly_hide: {
                 return new AlertDialog.Builder(mContext)
                         .setTitle(R.string.deleteConfirmation_title)
                         .setIcon(android.R.drawable.ic_dialog_alert)
@@ -918,28 +915,10 @@
                         .create();
             }
             default:
-                throw new IllegalArgumentException("Invalid dialogId: " + dialogId);
+                return null;
         }
     }
 
-    /* package */ void showDialog(int bundleDialogId) {
-        Bundle bundle = new Bundle();
-        bundle.putInt(DIALOG_ID_KEY, bundleDialogId);
-        getDialogManager().showDialogInView(mView, bundle);
-    }
-
-    private DialogManager getDialogManager() {
-        if (mDialogManager == null) {
-            if (!(mContext instanceof DialogManager.DialogShowingViewActivity)) {
-                throw new IllegalStateException(
-                        "View must be hosted in an Activity that implements " +
-                        "DialogManager.DialogShowingViewActivity");
-            }
-            mDialogManager = ((DialogManager.DialogShowingViewActivity)mContext).getDialogManager();
-        }
-        return mDialogManager;
-    }
-
     public boolean onContextItemSelected(MenuItem item) {
         switch (item.getItemId()) {
             case MENU_ITEM_MAKE_DEFAULT: {
@@ -1020,4 +999,21 @@
 
         return false;
     }
+
+    public static interface Callbacks {
+        /**
+         * Contact was not found, so somehow close this fragment.
+         */
+        public void closeBecauseContactNotFound();
+
+        /**
+         * User decided to go to Edit-Mode
+         */
+        public void editContact(Uri rawContactUri);
+
+        /**
+         * User clicked a single item (e.g. mail)
+         */
+        public void itemClicked(Intent intent);
+    }
 }
diff --git a/src/com/android/contacts/views/detail/ContactLoader.java b/src/com/android/contacts/views/detail/ContactDetailLoader.java
similarity index 88%
rename from src/com/android/contacts/views/detail/ContactLoader.java
rename to src/com/android/contacts/views/detail/ContactDetailLoader.java
index bc885df..147ec3c 100644
--- a/src/com/android/contacts/views/detail/ContactLoader.java
+++ b/src/com/android/contacts/views/detail/ContactDetailLoader.java
@@ -43,15 +43,14 @@
 /**
  * Loads a single Contact and all it constituent RawContacts.
  */
-public class ContactLoader extends Loader<ContactLoader.Result> {
-    private boolean mIsSynchronous;
+public class ContactDetailLoader extends Loader<ContactDetailLoader.Result> {
+    private static final String TAG = "ContactLoader";
+
     private Uri mLookupUri;
     private Result mContact;
     private ForceLoadContentObserver mObserver;
     private boolean mDestroyed;
 
-    private static final String TAG = "ContactLoader";
-
     public interface Callbacks {
         public void onContactLoaded(Result contact);
     }
@@ -65,6 +64,13 @@
          */
         public static final Result NOT_FOUND = new Result();
 
+        /**
+         * Singleton instance that represents an error, e.g. because of an invalid Uri
+         * TODO: We should come up with something nicer here. Maybe use an Either type so
+         * that we can capture the Exception?
+         */
+        public static final Result ERROR = new Result();
+
         private final Uri mLookupUri;
         private final String mLookupKey;
         private final Uri mUri;
@@ -225,32 +231,35 @@
         final static int CONTACT_STATUS_LABEL = 11;
     }
 
-
-    public final class LoadContactTask extends AsyncTask<Void, Void, Result> {
+    private final class LoadContactTask extends AsyncTask<Void, Void, Result> {
 
         @Override
         protected Result doInBackground(Void... args) {
-            final ContentResolver resolver = getContext().getContentResolver();
-            Uri uriCurrentFormat = convertLegacyIfNecessary(mLookupUri);
-            Result result = loadContactHeaderData(resolver, uriCurrentFormat);
-            if (result == Result.NOT_FOUND) {
-                // No record found. Try to lookup up a new record with the same lookupKey.
-                // We might have went through a sync where Ids changed
-                final Uri freshLookupUri = Contacts.getLookupUri(resolver, uriCurrentFormat);
-                result = loadContactHeaderData(resolver, freshLookupUri);
+            try {
+                final ContentResolver resolver = getContext().getContentResolver();
+                final Uri uriCurrentFormat = convertLegacyIfNecessary(mLookupUri);
+                Result result = loadContactHeaderData(resolver, uriCurrentFormat);
                 if (result == Result.NOT_FOUND) {
-                    // Still not found. We now believe this contact really does not exist
-                    Log.e(TAG, "invalid contact uri: " + mLookupUri);
-                    return Result.NOT_FOUND;
+                    // No record found. Try to lookup up a new record with the same lookupKey.
+                    // We might have went through a sync where Ids changed
+                    final Uri freshLookupUri = Contacts.getLookupUri(resolver, uriCurrentFormat);
+                    result = loadContactHeaderData(resolver, freshLookupUri);
+                    if (result == Result.NOT_FOUND) {
+                        // Still not found. We now believe this contact really does not exist
+                        Log.e(TAG, "invalid contact uri: " + mLookupUri);
+                        return Result.NOT_FOUND;
+                    }
                 }
+
+                // These queries could be run in parallel (we did this until froyo). But unless
+                // we actually have two database connections there is no performance gain
+                loadSocial(resolver, result);
+                loadRawContacts(resolver, result);
+
+                return result;
+            } catch (Exception e) {
+                return Result.ERROR;
             }
-
-            // These queries could be run in parallel (we did this until froyo). But unless
-            // we actually have two database connections there is no performance gain
-            loadSocial(resolver, result);
-            loadRawContacts(resolver, result);
-
-            return result;
         }
 
         /**
@@ -436,14 +445,17 @@
                     mObserver = new ForceLoadContentObserver();
                 }
                 Log.i(TAG, "Registering content observer for " + mLookupUri);
-                getContext().getContentResolver().registerContentObserver(
-                        mLookupUri, true, mObserver);
+
+                if (result != Result.ERROR && result != Result.NOT_FOUND) {
+                    getContext().getContentResolver().registerContentObserver(mLookupUri, true,
+                            mObserver);
+                }
                 deliverResult(result);
             }
         }
     }
 
-    public ContactLoader(Context context, Uri lookupUri) {
+    public ContactDetailLoader(Context context, Uri lookupUri) {
         super(context);
         mLookupUri = lookupUri;
     }
@@ -459,11 +471,7 @@
 
     @Override
     public void forceLoad() {
-        LoadContactTask task = new LoadContactTask();
-        if (mIsSynchronous) {
-            task.onPostExecute(task.doInBackground((Void[])null));
-            return;
-        }
+        final LoadContactTask task = new LoadContactTask();
         task.execute((Void[])null);
     }
 
@@ -480,9 +488,4 @@
         mContact = null;
         mDestroyed = true;
     }
-
-
-    public void setSynchronous(boolean value) {
-        mIsSynchronous = value;
-    }
 }
diff --git a/src/com/android/contacts/views/edit/ContactEditFragment.java b/src/com/android/contacts/views/edit/ContactEditFragment.java
new file mode 100644
index 0000000..4666c44
--- /dev/null
+++ b/src/com/android/contacts/views/edit/ContactEditFragment.java
@@ -0,0 +1,1160 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.views.edit;
+
+import com.android.contacts.JoinContactActivity;
+import com.android.contacts.R;
+import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.Editor;
+import com.android.contacts.model.EntityDelta;
+import com.android.contacts.model.EntityModifier;
+import com.android.contacts.model.EntitySet;
+import com.android.contacts.model.GoogleSource;
+import com.android.contacts.model.Sources;
+import com.android.contacts.model.ContactsSource.EditType;
+import com.android.contacts.model.Editor.EditorListener;
+import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.ui.ViewIdGenerator;
+import com.android.contacts.ui.widget.BaseContactEditorView;
+import com.android.contacts.ui.widget.PhotoEditorView;
+import com.android.contacts.util.EmptyService;
+import com.android.contacts.util.WeakAsyncTask;
+
+import android.accounts.Account;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.app.patterns.Loader;
+import android.app.patterns.LoaderManagingFragment;
+import android.content.ActivityNotFoundException;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.OperationApplicationException;
+import android.content.ContentProviderOperation.Builder;
+import android.content.DialogInterface.OnDismissListener;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.provider.MediaStore;
+import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.LinearLayout;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.io.File;
+import java.lang.ref.WeakReference;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+
+public class ContactEditFragment
+        extends LoaderManagingFragment<ContactEditLoader.Result> {
+    private static final String TAG = "ContactEditFragment";
+
+    private static final int LOADER_DATA = 1;
+
+    /** The launch code when picking a photo and the raw data is returned */
+    private static final int PHOTO_PICKED_WITH_DATA = 3021;
+
+    /** The launch code when a contact to join with is returned */
+    private static final int REQUEST_JOIN_CONTACT = 3022;
+
+    /** The launch code when taking a picture */
+    private static final int CAMERA_WITH_DATA = 3023;
+
+    private static final String KEY_EDIT_STATE = "state";
+    private static final String KEY_RAW_CONTACT_ID_REQUESTING_PHOTO = "photorequester";
+    private static final String KEY_VIEW_ID_GENERATOR = "viewidgenerator";
+    private static final String KEY_CURRENT_PHOTO_FILE = "currentphotofile";
+    private static final String KEY_QUERY_SELECTION = "queryselection";
+    private static final String KEY_QUERY_SELECTION_ARGS = "queryselectionargs";
+    private static final String KEY_CONTACT_ID_FOR_JOIN = "contactidforjoin";
+
+    private static final String BUNDLE_SELECT_ACCOUNT_LIST = "account_list";
+
+    public static final int SAVE_MODE_DEFAULT = 0;
+    public static final int SAVE_MODE_SPLIT = 1;
+    public static final int SAVE_MODE_JOIN = 2;
+
+    private long mRawContactIdRequestingPhoto = -1;
+
+    private static final int ICON_SIZE = 96;
+
+    private static final File PHOTO_DIR = new File(
+            Environment.getExternalStorageDirectory() + "/DCIM/Camera");
+
+    private final Context mContext;
+    private final View mView;
+    private final LayoutInflater mInflater;
+    private final EntityDeltaComparator mComparator = new EntityDeltaComparator();
+    private final String mAction;
+    private final Uri mUri;
+    private final String mMimeType;
+    private final Bundle mIntentExtras;
+    private final Callbacks mCallbacks;
+
+    private File mCurrentPhotoFile;
+
+    private String mQuerySelection;
+    private String[] mQuerySelectionArgs;
+
+    private long mContactIdForJoin;
+
+    private LinearLayout mContent;
+    private EntitySet mState;
+
+    private ViewIdGenerator mViewIdGenerator;
+
+    // TODO: We might not need to pass Context and View manually here as the framework should
+    // take care of this...?
+    public ContactEditFragment(Context context, View view, String action, Uri uri,
+            String mimeType, Bundle intentExtras, Callbacks callbacks) {
+        super();
+
+        if (callbacks == null) throw new IllegalArgumentException("callbacks must be given");
+
+        mContext = context;
+        mView = view;
+        mAction = action;
+        mUri = uri;
+        mMimeType = mimeType;
+        mIntentExtras = intentExtras;
+        mCallbacks = callbacks;
+        mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        // Build editor and listen for photo requests
+        mContent = (LinearLayout) view.findViewById(R.id.editors);
+
+        view.findViewById(R.id.btn_done).setOnClickListener(new OnClickListener() {
+            public void onClick(View v) {
+                doSaveAction(SAVE_MODE_DEFAULT);
+            }
+        });
+        view.findViewById(R.id.btn_discard).setOnClickListener(new OnClickListener() {
+            public void onClick(View v) {
+                mCallbacks.closeAfterRevert();
+            }
+        });
+    }
+
+    @Override
+    public void onCreate(Bundle savedState) {
+        // TODO: Currently savedState is always null (framework issue). Test once this is fixed
+        super.onCreate(savedState);
+
+        // Handle initial actions only when existing state missing
+        final boolean hasIncomingState =
+                savedState != null && savedState.containsKey(KEY_EDIT_STATE);
+
+        if (!hasIncomingState) {
+            if (Intent.ACTION_EDIT.equals(mAction)) {
+                // Read initial state from database
+                mCallbacks.setTitleTo(R.string.editContact_title_edit);
+                startLoading(LOADER_DATA, null);
+            } else if (Intent.ACTION_INSERT.equals(mAction)) {
+                mCallbacks.setTitleTo(R.string.editContact_title_insert);
+
+                // Load Accounts async so that we can present them
+                AsyncTask<Void, Void, ArrayList<Account>> loadAccountsTask =
+                        new AsyncTask<Void, Void, ArrayList<Account>>() {
+                            @Override
+                            protected ArrayList<Account> doInBackground(Void... params) {
+                                return Sources.getInstance(mContext).getAccounts(true);
+                            }
+                            @Override
+                            protected void onPostExecute(ArrayList<Account> result) {
+                                selectAccountAndCreateContact(result, true);
+                            }
+                };
+                loadAccountsTask.execute();
+            } else throw new IllegalArgumentException("Unknown Action String " + mAction +
+                    ". Only support " + Intent.ACTION_EDIT + " or " + Intent.ACTION_INSERT);
+        }
+
+        if (savedState == null) {
+            // If savedState is non-null, onRestoreInstanceState() will restore the generator.
+            mViewIdGenerator = new ViewIdGenerator();
+        }
+    }
+
+    @Override
+    protected Loader<ContactEditLoader.Result> onCreateLoader(int id, Bundle args) {
+        return new ContactEditLoader(mContext, mUri, mMimeType, mIntentExtras);
+    }
+
+    @Override
+    protected void onInitializeLoaders() {
+    }
+
+    @Override
+    protected void onLoadFinished(Loader<ContactEditLoader.Result> loader,
+            ContactEditLoader.Result data) {
+        if (data == ContactEditLoader.Result.NOT_FOUND) {
+            // Item has been deleted
+            Log.i(TAG, "No contact found. Closing fragment");
+            mCallbacks.closeBecauseContactNotFound();
+            return;
+        }
+        setData(data);
+    }
+
+    public void setData(ContactEditLoader.Result data) {
+        mState = data.getEntitySet();
+        bindEditors();
+    }
+
+    public void selectAccountAndCreateContact(ArrayList<Account> accounts, boolean isNewContact) {
+        // No Accounts available.  Create a phone-local contact.
+        if (accounts.isEmpty()) {
+            createContact(null, isNewContact);
+            return;  // Don't show a dialog.
+        }
+
+        // In the common case of a single account being writable, auto-select
+        // it without showing a dialog.
+        if (accounts.size() == 1) {
+            createContact(accounts.get(0), isNewContact);
+            return;  // Don't show a dialog.
+        }
+
+        Bundle bundle = new Bundle();
+        bundle.putParcelableArrayList(BUNDLE_SELECT_ACCOUNT_LIST, accounts);
+        getActivity().showDialog(R.id.edit_dialog_select_account, bundle);
+    }
+
+    /**
+     * @param account may be null to signal a device-local contact should
+     *     be created.
+     * @param prefillFromIntent If this is set, the intent extras will be used to prefill the fields
+     */
+    private void createContact(Account account, boolean prefillFromIntent) {
+        final Sources sources = Sources.getInstance(mContext);
+        final ContentValues values = new ContentValues();
+        if (account != null) {
+            values.put(RawContacts.ACCOUNT_NAME, account.name);
+            values.put(RawContacts.ACCOUNT_TYPE, account.type);
+        } else {
+            values.putNull(RawContacts.ACCOUNT_NAME);
+            values.putNull(RawContacts.ACCOUNT_TYPE);
+        }
+
+        // Parse any values from incoming intent
+        EntityDelta insert = new EntityDelta(ValuesDelta.fromAfter(values));
+        final ContactsSource source = sources.getInflatedSource(
+                account != null ? account.type : null,
+                ContactsSource.LEVEL_CONSTRAINTS);
+        EntityModifier.parseExtras(mContext, source, insert,
+                prefillFromIntent ? mIntentExtras : null);
+
+        // Ensure we have some default fields
+        EntityModifier.ensureKindExists(insert, source, Phone.CONTENT_ITEM_TYPE);
+        EntityModifier.ensureKindExists(insert, source, Email.CONTENT_ITEM_TYPE);
+
+        if (mState == null) {
+            // Create state if none exists yet
+            mState = EntitySet.fromSingle(insert);
+        } else {
+            // Add contact onto end of existing state
+            mState.add(insert);
+        }
+
+        bindEditors();
+    }
+
+    private void bindEditors() {
+        // Sort the editors
+        Collections.sort(mState, mComparator);
+
+        // Remove any existing editors and rebuild any visible
+        mContent.removeAllViews();
+
+        final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+        final Sources sources = Sources.getInstance(mContext);
+        int size = mState.size();
+        for (int i = 0; i < size; i++) {
+            // TODO ensure proper ordering of entities in the list
+            final EntityDelta entity = mState.get(i);
+            final ValuesDelta values = entity.getValues();
+            if (!values.isVisible()) continue;
+
+            final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
+            final ContactsSource source = sources.getInflatedSource(accountType,
+                    ContactsSource.LEVEL_CONSTRAINTS);
+            final long rawContactId = values.getAsLong(RawContacts._ID);
+
+            final BaseContactEditorView editor;
+            if (!source.readOnly) {
+                editor = (BaseContactEditorView) inflater.inflate(R.layout.item_contact_editor,
+                        mContent, false);
+            } else {
+                editor = (BaseContactEditorView) inflater.inflate(
+                        R.layout.item_read_only_contact_editor, mContent, false);
+            }
+            final PhotoEditorView photoEditor = editor.getPhotoEditor();
+            photoEditor.setEditorListener(new PhotoListener(rawContactId, source.readOnly,
+                    photoEditor));
+
+            mContent.addView(editor);
+            editor.setState(entity, source, mViewIdGenerator);
+        }
+
+        // Show editor now that we've loaded state
+        mContent.setVisibility(View.VISIBLE);
+    }
+
+    public void onClick(View v) {
+        // TODO forward to controller?
+    }
+
+    /**
+     * Pick a specific photo to be added under the currently selected tab.
+     */
+    /* package */ boolean doPickPhotoAction(long rawContactId) {
+        if (!hasValidState()) return false;
+
+        mRawContactIdRequestingPhoto = rawContactId;
+
+        getActivity().showDialog(R.id.edit_dialog_pick_photo);
+        return false;
+    }
+
+    public Dialog onCreateDialog(int id, Bundle bundle) {
+        switch (id) {
+            case R.id.edit_dialog_confirm_delete:
+                return new AlertDialog.Builder(mContext)
+                        .setTitle(R.string.deleteConfirmation_title)
+                        .setIcon(android.R.drawable.ic_dialog_alert)
+                        .setMessage(R.string.deleteConfirmation)
+                        .setNegativeButton(android.R.string.cancel, null)
+                        .setPositiveButton(android.R.string.ok, new DeleteClickListener())
+                        .setCancelable(false)
+                        .create();
+            case R.id.edit_dialog_confirm_readonly_delete:
+                return new AlertDialog.Builder(mContext)
+                        .setTitle(R.string.deleteConfirmation_title)
+                        .setIcon(android.R.drawable.ic_dialog_alert)
+                        .setMessage(R.string.readOnlyContactDeleteConfirmation)
+                        .setNegativeButton(android.R.string.cancel, null)
+                        .setPositiveButton(android.R.string.ok, new DeleteClickListener())
+                        .setCancelable(false)
+                        .create();
+            case R.id.edit_dialog_confirm_multiple_delete:
+                return new AlertDialog.Builder(mContext)
+                        .setTitle(R.string.deleteConfirmation_title)
+                        .setIcon(android.R.drawable.ic_dialog_alert)
+                        .setMessage(R.string.multipleContactDeleteConfirmation)
+                        .setNegativeButton(android.R.string.cancel, null)
+                        .setPositiveButton(android.R.string.ok, new DeleteClickListener())
+                        .setCancelable(false)
+                        .create();
+            case R.id.edit_dialog_confirm_readonly_hide:
+                return new AlertDialog.Builder(mContext)
+                        .setTitle(R.string.deleteConfirmation_title)
+                        .setIcon(android.R.drawable.ic_dialog_alert)
+                        .setMessage(R.string.readOnlyContactWarning)
+                        .setPositiveButton(android.R.string.ok, new DeleteClickListener())
+                        .setCancelable(false)
+                        .create();
+            case R.id.edit_dialog_pick_photo:
+                return createPickPhotoDialog();
+            case R.id.edit_dialog_split:
+                return createSplitDialog();
+            case R.id.edit_dialog_select_account:
+                return createSelectAccountDialog(bundle);
+            default:
+                return null;
+        }
+    }
+
+    private Dialog createSelectAccountDialog(Bundle bundle) {
+        final ArrayList<Account> accounts = bundle.getParcelableArrayList(
+                BUNDLE_SELECT_ACCOUNT_LIST);
+        // Wrap our context to inflate list items using correct theme
+        final Context dialogContext = new ContextThemeWrapper(mContext, android.R.style.Theme_Light);
+        final LayoutInflater dialogInflater =
+            (LayoutInflater)dialogContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+        final Sources sources = Sources.getInstance(mContext);
+
+        final ArrayAdapter<Account> accountAdapter = new ArrayAdapter<Account>(mContext,
+                android.R.layout.simple_list_item_2, accounts) {
+            @Override
+            public View getView(int position, View convertView, ViewGroup parent) {
+                if (convertView == null) {
+                    convertView = dialogInflater.inflate(android.R.layout.simple_list_item_2,
+                            parent, false);
+                }
+
+                // TODO: show icon along with title
+                final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
+                final TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
+
+                final Account account = this.getItem(position);
+                final ContactsSource source = sources.getInflatedSource(account.type,
+                        ContactsSource.LEVEL_SUMMARY);
+
+                text1.setText(account.name);
+                text2.setText(source.getDisplayLabel(mContext));
+
+                return convertView;
+            }
+        };
+
+        final DialogInterface.OnClickListener clickListener =
+                new DialogInterface.OnClickListener() {
+            public void onClick(DialogInterface dialog, int which) {
+                dialog.dismiss();
+
+                // Create new contact based on selected source
+                final Account account = accountAdapter.getItem(which);
+                createContact(account, false);
+            }
+        };
+
+        final DialogInterface.OnCancelListener cancelListener =
+                new DialogInterface.OnCancelListener() {
+            public void onCancel(DialogInterface dialog) {
+                // If nothing remains, close activity
+                if (!hasValidState()) {
+                    // TODO: pass this back to Activity
+                    // finish();
+                }
+            }
+        };
+
+        final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
+        builder.setTitle(R.string.dialog_new_contact_account);
+        builder.setSingleChoiceItems(accountAdapter, 0, clickListener);
+        builder.setOnCancelListener(cancelListener);
+        final Dialog result = builder.create();
+        result.setOnDismissListener(new OnDismissListener() {
+            public void onDismiss(DialogInterface dialog) {
+                // TODO: Check if we even need this...seems useless
+                //removeDialog(DIALOG_SELECT_ACCOUNT);
+            }
+        });
+        return result;
+    }
+
+    private Dialog createSplitDialog() {
+        final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
+        builder.setTitle(R.string.splitConfirmation_title);
+        builder.setIcon(android.R.drawable.ic_dialog_alert);
+        builder.setMessage(R.string.splitConfirmation);
+        builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+            public void onClick(DialogInterface dialog, int which) {
+                // Split the contacts
+                mState.splitRawContacts();
+                doSaveAction(SAVE_MODE_SPLIT);
+            }
+        });
+        builder.setNegativeButton(android.R.string.cancel, null);
+        builder.setCancelable(false);
+        return builder.create();
+    }
+
+    /**
+     * Creates a dialog offering two options: take a photo or pick a photo from the gallery.
+     */
+    private Dialog createPickPhotoDialog() {
+        // Wrap our context to inflate list items using correct theme
+        final Context dialogContext = new ContextThemeWrapper(mContext,
+                android.R.style.Theme_Light);
+
+        String[] choices = new String[2];
+        choices[0] = mContext.getString(R.string.take_photo);
+        choices[1] = mContext.getString(R.string.pick_photo);
+        final ListAdapter adapter = new ArrayAdapter<String>(dialogContext,
+                android.R.layout.simple_list_item_1, choices);
+
+        final AlertDialog.Builder builder = new AlertDialog.Builder(dialogContext);
+        builder.setTitle(R.string.attachToContact);
+        builder.setSingleChoiceItems(adapter, -1, new DialogInterface.OnClickListener() {
+            public void onClick(DialogInterface dialog, int which) {
+                dialog.dismiss();
+                switch(which) {
+                    case 0:
+                        doTakePhoto();
+                        break;
+                    case 1:
+                        doPickPhotoFromGallery();
+                        break;
+                }
+            }
+        });
+        return builder.create();
+    }
+
+
+    /**
+     * Launches Gallery to pick a photo.
+     */
+    protected void doPickPhotoFromGallery() {
+        try {
+            // Launch picker to choose photo for selected contact
+            final Intent intent = getPhotoPickIntent();
+            // TODO: Do this again (Controller?)
+            //startActivityForResult(intent, PHOTO_PICKED_WITH_DATA);
+        } catch (ActivityNotFoundException e) {
+            Toast.makeText(mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
+        }
+    }
+
+    /**
+     * Constructs an intent for picking a photo from Gallery, cropping it and returning the bitmap.
+     */
+    public static Intent getPhotoPickIntent() {
+        Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
+        intent.setType("image/*");
+        intent.putExtra("crop", "true");
+        intent.putExtra("aspectX", 1);
+        intent.putExtra("aspectY", 1);
+        intent.putExtra("outputX", ICON_SIZE);
+        intent.putExtra("outputY", ICON_SIZE);
+        intent.putExtra("return-data", true);
+        return intent;
+    }
+
+    /**
+     * Check if our internal {@link #mState} is valid, usually checked before
+     * performing user actions.
+     */
+    private boolean hasValidState() {
+        return mState != null && mState.size() > 0;
+    }
+
+    /**
+     * Create a file name for the icon photo using current time.
+     */
+    private String getPhotoFileName() {
+        Date date = new Date(System.currentTimeMillis());
+        SimpleDateFormat dateFormat = new SimpleDateFormat("'IMG'_yyyyMMdd_HHmmss");
+        return dateFormat.format(date) + ".jpg";
+    }
+
+    /**
+     * Launches Camera to take a picture and store it in a file.
+     */
+    protected void doTakePhoto() {
+        try {
+            // Launch camera to take photo for selected contact
+            PHOTO_DIR.mkdirs();
+            mCurrentPhotoFile = new File(PHOTO_DIR, getPhotoFileName());
+            final Intent intent = getTakePickIntent(mCurrentPhotoFile);
+
+            // TODO: Start camera
+            //startActivityForResult(intent, CAMERA_WITH_DATA);
+        } catch (ActivityNotFoundException e) {
+            Toast.makeText(mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
+        }
+    }
+
+    /**
+     * Constructs an intent for capturing a photo and storing it in a temporary file.
+     */
+    public static Intent getTakePickIntent(File f) {
+        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE, null);
+        intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(f));
+        return intent;
+    }
+
+    /**
+     * Saves or creates the contact based on the mode, and if successful
+     * finishes the activity.
+     */
+    private boolean doSaveAction(int saveMode) {
+        if (!hasValidState()) {
+            return false;
+        }
+
+        // TODO: Status still needed?
+        //mStatus = STATUS_SAVING;
+        final PersistTask task = new PersistTask(this, saveMode);
+        task.execute(mState);
+
+        return true;
+    }
+
+    private void onSaveCompleted(boolean success, int saveMode, Uri contactLookupUri) {
+        switch (saveMode) {
+            case SAVE_MODE_DEFAULT:
+                final Intent resultIntent;
+                final int resultCode;
+                if (success && contactLookupUri != null) {
+                    final String requestAuthority = mUri == null ? null : mUri.getAuthority();
+
+                    final String legacyAuthority = "contacts";
+
+                    resultIntent = new Intent();
+                    if (legacyAuthority.equals(requestAuthority)) {
+                        // Build legacy Uri when requested by caller
+                        final long contactId = ContentUris.parseId(Contacts.lookupContact(
+                                mContext.getContentResolver(), contactLookupUri));
+                        final Uri legacyContentUri = Uri.parse("content://contacts/people");
+                        final Uri legacyUri = ContentUris.withAppendedId(
+                                legacyContentUri, contactId);
+                        resultIntent.setData(legacyUri);
+                    } else {
+                        // Otherwise pass back a lookup-style Uri
+                        resultIntent.setData(contactLookupUri);
+                    }
+
+                    resultCode = Activity.RESULT_OK;
+                } else {
+                    resultCode = Activity.RESULT_CANCELED;
+                    resultIntent = null;
+                }
+                mCallbacks.closeAfterSaving(resultCode, resultIntent);
+                break;
+// TODO: Other save modes
+
+//            case SAVE_MODE_SPLIT:
+//                if (success) {
+//                    Intent intent = new Intent();
+//                    intent.setData(contactLookupUri);
+//                    setResult(RESULT_CLOSE_VIEW_ACTIVITY, intent);
+//                }
+//                finish();
+//                break;
+//
+//            case SAVE_MODE_JOIN:
+//                mStatus = STATUS_EDITING;
+//                if (success) {
+//                    showJoinAggregateActivity(contactLookupUri);
+//                }
+//                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)
+     */
+    public void showJoinAggregateActivity(Uri contactLookupUri) {
+        if (contactLookupUri == null) {
+            return;
+        }
+
+        mContactIdForJoin = ContentUris.parseId(contactLookupUri);
+        Intent intent = new Intent(JoinContactActivity.JOIN_CONTACT);
+        intent.putExtra(JoinContactActivity.EXTRA_TARGET_CONTACT_ID, mContactIdForJoin);
+        getActivity().startActivityForResult(intent, REQUEST_JOIN_CONTACT);
+    }
+
+    private interface JoinContactQuery {
+        String[] PROJECTION = {
+                RawContacts._ID,
+                RawContacts.CONTACT_ID,
+                RawContacts.NAME_VERIFIED,
+        };
+
+        String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
+
+        int _ID = 0;
+        int CONTACT_ID = 1;
+        int NAME_VERIFIED = 2;
+    }
+
+    /**
+     * Performs aggregation with the contact selected by the user from suggestions or A-Z list.
+     */
+    private void joinAggregate(final long contactId) {
+        final ContentResolver resolver = mContext.getContentResolver();
+
+        // Load raw contact IDs for all raw contacts involved - currently edited and selected
+        // in the join UIs
+        Cursor c = resolver.query(RawContacts.CONTENT_URI,
+                JoinContactQuery.PROJECTION,
+                JoinContactQuery.SELECTION,
+                new String[]{String.valueOf(contactId), String.valueOf(mContactIdForJoin)}, null);
+
+        long rawContactIds[];
+        long verifiedNameRawContactId = -1;
+        try {
+            rawContactIds = new long[c.getCount()];
+            for (int i = 0; i < rawContactIds.length; i++) {
+                c.moveToNext();
+                long rawContactId = c.getLong(JoinContactQuery._ID);
+                rawContactIds[i] = rawContactId;
+                if (c.getLong(JoinContactQuery.CONTACT_ID) == mContactIdForJoin) {
+                    if (verifiedNameRawContactId == -1
+                            || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0) {
+                        verifiedNameRawContactId = rawContactId;
+                    }
+                }
+            }
+        } finally {
+            c.close();
+        }
+
+        // For each pair of raw contacts, insert an aggregation exception
+        ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
+        for (int i = 0; i < rawContactIds.length; i++) {
+            for (int j = 0; j < rawContactIds.length; j++) {
+                if (i != j) {
+                    buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
+                }
+            }
+        }
+
+        // Mark the original contact as "name verified" to make sure that the contact
+        // display name does not change as a result of the join
+        Builder builder = ContentProviderOperation.newUpdate(
+                    ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
+        builder.withValue(RawContacts.NAME_VERIFIED, 1);
+        operations.add(builder.build());
+
+        // Apply all aggregation exceptions as one batch
+        try {
+            resolver.applyBatch(ContactsContract.AUTHORITY, operations);
+
+            // We can use any of the constituent raw contacts to refresh the UI - why not the first
+            final Intent intent = new Intent();
+            intent.setData(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
+
+            // Reload the new state from database
+            // TODO: Reload necessary or do we have a listener?
+            //new QueryEntitiesTask(this).execute(intent);
+
+            Toast.makeText(mContext, R.string.contactsJoinedMessage, Toast.LENGTH_LONG).show();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to apply aggregation exception batch", e);
+            Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
+        } catch (OperationApplicationException e) {
+            Log.e(TAG, "Failed to apply aggregation exception batch", e);
+            Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
+        }
+    }
+
+    /**
+     * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
+     */
+    private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
+            long rawContactId1, long rawContactId2) {
+        Builder builder =
+                ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
+        builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
+        builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
+        builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
+        operations.add(builder.build());
+    }
+
+    public static interface Callbacks {
+        /**
+         * Contact was not found, so somehow close this fragment.
+         */
+        void closeBecauseContactNotFound();
+
+        /**
+         * User has tapped Revert, close the fragment now.
+         */
+        void closeAfterRevert();
+
+        /**
+         * Set the Title (e.g. of the Activity)
+         */
+        void setTitleTo(int resourceId);
+
+        /**
+         * Contact was
+         * @param resultCode
+         * @param resultIntent
+         */
+        void closeAfterSaving(int resultCode, Intent resultIntent);
+    }
+
+    private class EntityDeltaComparator implements Comparator<EntityDelta> {
+        /**
+         * Compare EntityDeltas for sorting the stack of editors.
+         */
+        public int compare(EntityDelta one, EntityDelta two) {
+            // Check direct equality
+            if (one.equals(two)) {
+                return 0;
+            }
+
+            final Sources sources = Sources.getInstance(mContext);
+            String accountType = one.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+            final ContactsSource oneSource = sources.getInflatedSource(accountType,
+                    ContactsSource.LEVEL_SUMMARY);
+            accountType = two.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+            final ContactsSource twoSource = sources.getInflatedSource(accountType,
+                    ContactsSource.LEVEL_SUMMARY);
+
+            // Check read-only
+            if (oneSource.readOnly && !twoSource.readOnly) {
+                return 1;
+            } else if (twoSource.readOnly && !oneSource.readOnly) {
+                return -1;
+            }
+
+            // Check account type
+            boolean skipAccountTypeCheck = false;
+            boolean oneIsGoogle = oneSource instanceof GoogleSource;
+            boolean twoIsGoogle = twoSource instanceof GoogleSource;
+            if (oneIsGoogle && !twoIsGoogle) {
+                return -1;
+            } else if (twoIsGoogle && !oneIsGoogle) {
+                return 1;
+            } else if (oneIsGoogle && twoIsGoogle){
+                skipAccountTypeCheck = true;
+            }
+
+            int value;
+            if (!skipAccountTypeCheck) {
+                value = oneSource.accountType.compareTo(twoSource.accountType);
+                if (value != 0) {
+                    return value;
+                }
+            }
+
+            // Check account name
+            ValuesDelta oneValues = one.getValues();
+            String oneAccount = oneValues.getAsString(RawContacts.ACCOUNT_NAME);
+            if (oneAccount == null) oneAccount = "";
+            ValuesDelta twoValues = two.getValues();
+            String twoAccount = twoValues.getAsString(RawContacts.ACCOUNT_NAME);
+            if (twoAccount == null) twoAccount = "";
+            value = oneAccount.compareTo(twoAccount);
+            if (value != 0) {
+                return value;
+            }
+
+            // Both are in the same account, fall back to contact ID
+            Long oneId = oneValues.getAsLong(RawContacts._ID);
+            Long twoId = twoValues.getAsLong(RawContacts._ID);
+            if (oneId == null) {
+                return -1;
+            } else if (twoId == null) {
+                return 1;
+            }
+
+            return (int)(oneId - twoId);
+        }
+    }
+
+    /**
+     * Class that listens to requests coming from photo editors
+     */
+    private class PhotoListener implements EditorListener, DialogInterface.OnClickListener {
+        private long mRawContactId;
+        private boolean mReadOnly;
+        private PhotoEditorView mEditor;
+
+        public PhotoListener(long rawContactId, boolean readOnly, PhotoEditorView editor) {
+            mRawContactId = rawContactId;
+            mReadOnly = readOnly;
+            mEditor = editor;
+        }
+
+        public void onDeleted(Editor editor) {
+            // Do nothing
+        }
+
+        public void onRequest(int request) {
+            // TODO: Still needed?
+//            if (!hasValidState()) return;
+
+            if (request == EditorListener.REQUEST_PICK_PHOTO) {
+                if (mEditor.hasSetPhoto()) {
+                    // There is an existing photo, offer to remove, replace, or promoto to primary
+                    createPhotoDialog().show();
+                } else if (!mReadOnly) {
+                    // No photo set and not read-only, try to set the photo
+                    doPickPhotoAction(mRawContactId);
+                }
+            }
+        }
+
+        /**
+         * Prepare dialog for picking a new {@link EditType} or entering a
+         * custom label. This dialog is limited to the valid types as determined
+         * by {@link EntityModifier}.
+         */
+        public Dialog createPhotoDialog() {
+            // Wrap our context to inflate list items using correct theme
+            final Context dialogContext = new ContextThemeWrapper(mContext,
+                    android.R.style.Theme_Light);
+
+            String[] choices;
+            if (mReadOnly) {
+                choices = new String[1];
+                choices[0] = mContext.getString(R.string.use_photo_as_primary);
+            } else {
+                choices = new String[3];
+                choices[0] = mContext.getString(R.string.use_photo_as_primary);
+                choices[1] = mContext.getString(R.string.removePicture);
+                choices[2] = mContext.getString(R.string.changePicture);
+            }
+            final ListAdapter adapter = new ArrayAdapter<String>(dialogContext,
+                    android.R.layout.simple_list_item_1, choices);
+
+            final AlertDialog.Builder builder = new AlertDialog.Builder(dialogContext);
+            builder.setTitle(R.string.attachToContact);
+            builder.setSingleChoiceItems(adapter, -1, this);
+            return builder.create();
+        }
+
+        /**
+         * Called when something in the dialog is clicked
+         */
+        public void onClick(DialogInterface dialog, int which) {
+            dialog.dismiss();
+
+            switch (which) {
+                case 0:
+                    // Set the photo as super primary
+                    mEditor.setSuperPrimary(true);
+
+                    // And set all other photos as not super primary
+                    int count = mContent.getChildCount();
+                    for (int i = 0; i < count; i++) {
+                        View childView = mContent.getChildAt(i);
+                        if (childView instanceof BaseContactEditorView) {
+                            BaseContactEditorView editor = (BaseContactEditorView) childView;
+                            PhotoEditorView photoEditor = editor.getPhotoEditor();
+                            if (!photoEditor.equals(mEditor)) {
+                                photoEditor.setSuperPrimary(false);
+                            }
+                        }
+                    }
+                    break;
+
+                case 1:
+                    // Remove the photo
+                    mEditor.setPhotoBitmap(null);
+                    break;
+
+                case 2:
+                    // Pick a new photo for the contact
+                    doPickPhotoAction(mRawContactId);
+                    break;
+            }
+        }
+    }
+
+
+    private class DeleteClickListener implements DialogInterface.OnClickListener {
+        public void onClick(DialogInterface dialog, int which) {
+            // TODO: Delete
+//            Sources sources = Sources.getInstance(mContext);
+//            // Mark all raw contacts for deletion
+//            for (EntityDelta delta : mState) {
+//                delta.markDeleted();
+//            }
+//            // Save the deletes
+//            doSaveAction(SAVE_MODE_DEFAULT);
+//            finish();
+        }
+    }
+
+    // TODO: There has to be a nicer way than this WeakAsyncTask...? Maybe call a service?
+    /**
+     * Background task for persisting edited contact data, using the changes
+     * defined by a set of {@link EntityDelta}. This task starts
+     * {@link EmptyService} to make sure the background thread can finish
+     * persisting in cases where the system wants to reclaim our process.
+     */
+    public static class PersistTask extends
+            WeakAsyncTask<EntitySet, Void, Integer, ContactEditFragment> {
+        private static final int PERSIST_TRIES = 3;
+
+        private static final int RESULT_UNCHANGED = 0;
+        private static final int RESULT_SUCCESS = 1;
+        private static final int RESULT_FAILURE = 2;
+
+        private WeakReference<ProgressDialog> mProgress;
+        private final Context mContext;
+
+        private int mSaveMode;
+        private Uri mContactLookupUri = null;
+
+        public PersistTask(ContactEditFragment target, int saveMode) {
+            super(target);
+            mSaveMode = saveMode;
+            mContext = target.mContext;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected void onPreExecute(ContactEditFragment target) {
+            mProgress = new WeakReference<ProgressDialog>(ProgressDialog.show(mContext, null,
+                    mContext.getText(R.string.savingContact)));
+
+            // Before starting this task, start an empty service to protect our
+            // process from being reclaimed by the system.
+            mContext.startService(new Intent(mContext, EmptyService.class));
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected Integer doInBackground(ContactEditFragment target, EntitySet... params) {
+            final ContentResolver resolver = mContext.getContentResolver();
+
+            EntitySet state = params[0];
+
+            // Trim any empty fields, and RawContacts, before persisting
+            final Sources sources = Sources.getInstance(mContext);
+            EntityModifier.trimEmpty(state, sources);
+
+            // Attempt to persist changes
+            int tries = 0;
+            Integer result = RESULT_FAILURE;
+            while (tries++ < PERSIST_TRIES) {
+                try {
+                    // Build operations and try applying
+                    final ArrayList<ContentProviderOperation> diff = state.buildDiff();
+                    ContentProviderResult[] results = null;
+                    if (!diff.isEmpty()) {
+                         results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
+                    }
+
+                    final long rawContactId = getRawContactId(state, diff, results);
+                    if (rawContactId != -1) {
+                        final Uri rawContactUri = ContentUris.withAppendedId(
+                                RawContacts.CONTENT_URI, rawContactId);
+
+                        // convert the raw contact URI to a contact URI
+                        mContactLookupUri = RawContacts.getContactLookupUri(resolver,
+                                rawContactUri);
+                    }
+                    result = (diff.size() > 0) ? RESULT_SUCCESS : RESULT_UNCHANGED;
+                    break;
+
+                } catch (RemoteException e) {
+                    // Something went wrong, bail without success
+                    Log.e(TAG, "Problem persisting user edits", e);
+                    break;
+
+                } catch (OperationApplicationException e) {
+                    // Version consistency failed, re-parent change and try again
+                    Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
+                    final EntitySet newState = EntitySet.fromQuery(resolver,
+                            target.mQuerySelection, target.mQuerySelectionArgs, null);
+                    state = EntitySet.mergeAfter(newState, state);
+                }
+            }
+
+            return result;
+        }
+
+        private long getRawContactId(EntitySet state,
+                final ArrayList<ContentProviderOperation> diff,
+                final ContentProviderResult[] results) {
+            long rawContactId = state.findRawContactId();
+            if (rawContactId != -1) {
+                return rawContactId;
+            }
+
+            // we gotta do some searching for the id
+            final int diffSize = diff.size();
+            for (int i = 0; i < diffSize; i++) {
+                ContentProviderOperation operation = diff.get(i);
+                if (operation.getType() == ContentProviderOperation.TYPE_INSERT
+                        && operation.getUri().getEncodedPath().contains(
+                                RawContacts.CONTENT_URI.getEncodedPath())) {
+                    return ContentUris.parseId(results[i].uri);
+                }
+            }
+            return -1;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected void onPostExecute(ContactEditFragment target, Integer result) {
+            final ProgressDialog progress = mProgress.get();
+
+            if (result == RESULT_SUCCESS && mSaveMode != SAVE_MODE_JOIN) {
+                Toast.makeText(mContext, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
+            } else if (result == RESULT_FAILURE) {
+                Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
+            }
+
+            // TODO: How do we get rid of the dialog
+//            dismissDialog(progress);
+
+            // Stop the service that was protecting us
+            mContext.stopService(new Intent(mContext, EmptyService.class));
+
+            target.onSaveCompleted(result != RESULT_FAILURE, mSaveMode, mContactLookupUri);
+        }
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        if (hasValidState()) {
+            // Store entities with modifications
+            outState.putParcelable(KEY_EDIT_STATE, mState);
+        }
+
+        outState.putLong(KEY_RAW_CONTACT_ID_REQUESTING_PHOTO, mRawContactIdRequestingPhoto);
+        outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator);
+        if (mCurrentPhotoFile != null) {
+            outState.putString(KEY_CURRENT_PHOTO_FILE, mCurrentPhotoFile.toString());
+        }
+        outState.putString(KEY_QUERY_SELECTION, mQuerySelection);
+        outState.putStringArray(KEY_QUERY_SELECTION_ARGS, mQuerySelectionArgs);
+        outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin);
+        super.onSaveInstanceState(outState);
+    }
+
+    @Override
+    public void onRestoreInstanceState(Bundle savedInstanceState) {
+        // Read modifications from instance
+        mState = savedInstanceState.<EntitySet> getParcelable(KEY_EDIT_STATE);
+        mRawContactIdRequestingPhoto = savedInstanceState.getLong(
+                KEY_RAW_CONTACT_ID_REQUESTING_PHOTO);
+        mViewIdGenerator = savedInstanceState.getParcelable(KEY_VIEW_ID_GENERATOR);
+        String fileName = savedInstanceState.getString(KEY_CURRENT_PHOTO_FILE);
+        if (fileName != null) {
+            mCurrentPhotoFile = new File(fileName);
+        }
+        mQuerySelection = savedInstanceState.getString(KEY_QUERY_SELECTION);
+        mQuerySelectionArgs = savedInstanceState.getStringArray(KEY_QUERY_SELECTION_ARGS);
+        mContactIdForJoin = savedInstanceState.getLong(KEY_CONTACT_ID_FOR_JOIN);
+
+        bindEditors();
+
+        super.onRestoreInstanceState(savedInstanceState);
+    }
+}
diff --git a/src/com/android/contacts/views/edit/ContactEditLoader.java b/src/com/android/contacts/views/edit/ContactEditLoader.java
new file mode 100644
index 0000000..3506c59
--- /dev/null
+++ b/src/com/android/contacts/views/edit/ContactEditLoader.java
@@ -0,0 +1,177 @@
+package com.android.contacts.views.edit;
+
+import com.android.contacts.ContactsUtils;
+import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.EntityDelta;
+import com.android.contacts.model.EntityModifier;
+import com.android.contacts.model.EntitySet;
+import com.android.contacts.model.Sources;
+
+import android.app.patterns.Loader;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+
+public class ContactEditLoader extends Loader<ContactEditLoader.Result> {
+    private static final String TAG = "ContactEditLoader";
+
+    private final Uri mLookupUri;
+    private final String mMimeType;
+    private Result mContact;
+    private boolean mDestroyed;
+    private ForceLoadContentObserver mObserver;
+    private final Bundle mIntentExtras;
+
+    public ContactEditLoader(Context context, Uri lookupUri, String mimeType,
+            Bundle intentExtras) {
+        super(context);
+        mLookupUri = lookupUri;
+        mMimeType = mimeType;
+        mIntentExtras = intentExtras;
+    }
+
+    /**
+     * The result of a load operation. Contains all data necessary to display the contact for
+     * editing.
+     */
+    public static class Result {
+        /**
+         * Singleton instance that represents "No Contact Found"
+         */
+        public static final Result NOT_FOUND = new Result(null);
+
+        private final EntitySet mEntitySet;
+
+        private Result(EntitySet entitySet) {
+            mEntitySet = entitySet;
+        }
+
+        public EntitySet getEntitySet() {
+            return mEntitySet;
+        }
+    }
+
+    private final class LoadContactTask extends AsyncTask<Void, Void, Result> {
+        @Override
+        protected Result doInBackground(Void... params) {
+            final ContentResolver resolver = getContext().getContentResolver();
+            final Uri uriCurrentFormat = convertLegacyIfNecessary(mLookupUri);
+
+            // Handle both legacy and new authorities
+
+            final long contactId;
+            final String selection = "0";
+            if (Contacts.CONTENT_ITEM_TYPE.equals(mMimeType)) {
+                // Handle selected aggregate
+                contactId = ContentUris.parseId(uriCurrentFormat);
+            } else if (RawContacts.CONTENT_ITEM_TYPE.equals(mMimeType)) {
+                // Get id of corresponding aggregate
+                final long rawContactId = ContentUris.parseId(uriCurrentFormat);
+                contactId = ContactsUtils.queryForContactId(resolver, rawContactId);
+            } else throw new IllegalStateException();
+
+            return new Result(EntitySet.fromQuery(resolver, RawContacts.CONTACT_ID + "=?",
+                    new String[] { String.valueOf(contactId) }, null));
+        }
+
+        /**
+         * Transforms the given Uri and returns a Lookup-Uri that represents the contact.
+         * For legacy contacts, a raw-contact lookup is performed.
+         */
+        private Uri convertLegacyIfNecessary(Uri uri) {
+            if (uri == null) throw new IllegalArgumentException("uri must not be null");
+
+            final String authority = uri.getAuthority();
+
+            // Current Style Uri? Just return it
+            if (ContactsContract.AUTHORITY.equals(authority)) {
+                return uri;
+            }
+
+            // Legacy Style? Convert to RawContact
+            final String OBSOLETE_AUTHORITY = "contacts";
+            if (OBSOLETE_AUTHORITY.equals(authority)) {
+                // Legacy Format. Convert to RawContact-Uri and then lookup the contact
+                final long rawContactId = ContentUris.parseId(uri);
+                return RawContacts.getContactLookupUri(getContext().getContentResolver(),
+                        ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
+            }
+
+            throw new IllegalArgumentException("uri format is unknown");
+        }
+
+        @Override
+        protected void onPostExecute(Result result) {
+            super.onPostExecute(result);
+
+            // TODO: This merging of extras is probably wrong on subsequent loads
+
+            // Load edit details in background
+            final Sources sources = Sources.getInstance(getContext());
+
+            // Handle any incoming values that should be inserted
+            final boolean hasExtras = mIntentExtras != null && mIntentExtras.size() > 0;
+            final boolean hasState = result.getEntitySet().size() > 0;
+            if (hasExtras && hasState) {
+                // Find source defining the first RawContact found
+                final EntityDelta state = result.getEntitySet().get(0);
+                final String accountType = state.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+                final ContactsSource source = sources.getInflatedSource(accountType,
+                        ContactsSource.LEVEL_CONSTRAINTS);
+                EntityModifier.parseExtras(getContext(), source, state, mIntentExtras);
+            }
+
+            // The creator isn't interested in any further updates
+            if (mDestroyed) {
+                return;
+            }
+
+            mContact = result;
+            if (result != null) {
+                if (mObserver == null) {
+                    mObserver = new ForceLoadContentObserver();
+                }
+                // TODO: Do we want a content observer here?
+//                Log.i(TAG, "Registering content observer for " + mLookupUri);
+//                getContext().getContentResolver().registerContentObserver(mLookupUri, true,
+//                        mObserver);
+                deliverResult(result);
+            }
+        }
+    }
+
+    @Override
+    public void startLoading() {
+        if (mContact != null) {
+            deliverResult(mContact);
+        } else {
+            forceLoad();
+        }
+    }
+
+    @Override
+    public void forceLoad() {
+        LoadContactTask task = new LoadContactTask();
+        task.execute((Void[])null);
+    }
+
+    @Override
+    public void stopLoading() {
+        mContact = null;
+        if (mObserver != null) {
+            getContext().getContentResolver().unregisterContentObserver(mObserver);
+        }
+    }
+
+    @Override
+    public void destroy() {
+        mContact = null;
+        mDestroyed = true;
+    }
+}
diff --git a/tests/src/com/android/contacts/ContactDetailLoaderTest.java b/tests/src/com/android/contacts/ContactDetailLoaderTest.java
index 6676a2b..77cfb1f 100644
--- a/tests/src/com/android/contacts/ContactDetailLoaderTest.java
+++ b/tests/src/com/android/contacts/ContactDetailLoaderTest.java
@@ -18,7 +18,7 @@
 
 import com.android.contacts.tests.mocks.ContactsMockContext;
 import com.android.contacts.tests.mocks.MockContentProvider;
-import com.android.contacts.views.detail.ContactLoader;
+import com.android.contacts.views.detail.ContactDetailLoader;
 
 import android.app.patterns.Loader;
 import android.app.patterns.Loader.OnLoadCompleteListener;
@@ -59,7 +59,7 @@
             protected void onPostExecute(Void result) {}
         };
     }
-    
+
     @Override
     protected void setUp() throws Exception {
         super.setUp();
@@ -144,42 +144,24 @@
         return result;
     }
 
-    private ContactLoader.Result assertLoadContact(Uri uri) {
-        final ContactLoader loader = new ContactLoader(mMockContext, uri);
+    private ContactDetailLoader.Result assertLoadContact(Uri uri) {
+        final ContactDetailLoader loader = new ContactDetailLoader(mMockContext, uri);
         return getLoaderResultSynchronously(loader);
     }
 
-    @Suppress // The code under test is incorrect
     public void testNullUri() {
-        IllegalArgumentException e =
-            assertThrows(IllegalArgumentException.class, new Runnable() {
-                public void run() {
-                    assertLoadContact(null);
-                }
-            });
-        assertEquals(e.getMessage(), "uri must not be null");
+        ContactDetailLoader.Result result = assertLoadContact(null);
+        assertEquals(ContactDetailLoader.Result.ERROR, result);
     }
 
-    @Suppress // The code under test is incorrect
     public void testEmptyUri() {
-        IllegalArgumentException e =
-            assertThrows(IllegalArgumentException.class, new Runnable() {
-                public void run() {
-                    assertLoadContact(Uri.EMPTY);
-                }
-            });
-        assertEquals(e.getMessage(), "uri format is unknown");
+        ContactDetailLoader.Result result = assertLoadContact(Uri.EMPTY);
+        assertEquals(ContactDetailLoader.Result.ERROR, result);
     }
 
-    @Suppress // The code under test is incorrect
     public void testInvalidUri() {
-        IllegalArgumentException e =
-                assertThrows(IllegalArgumentException.class, new Runnable() {
-                    public void run() {
-                        assertLoadContact(Uri.parse("content://wtf"));
-                    }
-                });
-        assertEquals(e.getMessage(), "uri format is unknown");
+        ContactDetailLoader.Result result = assertLoadContact(Uri.parse("content://wtf"));
+        assertEquals(ContactDetailLoader.Result.ERROR, result);
     }
 
     public void testLoadContactWithContactIdUri() {
@@ -201,7 +183,7 @@
         queries.fetchSocial(dataUri, contactId);
         queries.fetchRawContacts(contactId, dataId, rawContactId);
 
-        ContactLoader.Result contact = assertLoadContact(baseUri);
+        ContactDetailLoader.Result contact = assertLoadContact(baseUri);
 
         assertEquals(contactId, contact.getId());
         assertEquals(rawContactId, contact.getNameRawContactId());
@@ -235,7 +217,7 @@
         queries.fetchSocial(dataUri, contactId);
         queries.fetchRawContacts(contactId, dataId, rawContactId);
 
-        ContactLoader.Result contact = assertLoadContact(legacyUri);
+        ContactDetailLoader.Result contact = assertLoadContact(legacyUri);
 
         assertEquals(contactId, contact.getId());
         assertEquals(rawContactId, contact.getNameRawContactId());
@@ -266,7 +248,7 @@
         queries.fetchSocial(dataUri, contactId);
         queries.fetchRawContacts(contactId, dataId, rawContactId);
 
-        ContactLoader.Result contact = assertLoadContact(lookupNoIdUri);
+        ContactDetailLoader.Result contact = assertLoadContact(lookupNoIdUri);
 
         assertEquals(contactId, contact.getId());
         assertEquals(rawContactId, contact.getNameRawContactId());
@@ -296,7 +278,7 @@
         queries.fetchSocial(dataUri, contactId);
         queries.fetchRawContacts(contactId, dataId, rawContactId);
 
-        ContactLoader.Result contact = assertLoadContact(lookupUri);
+        ContactDetailLoader.Result contact = assertLoadContact(lookupUri);
 
         assertEquals(contactId, contact.getId());
         assertEquals(rawContactId, contact.getNameRawContactId());
@@ -338,7 +320,7 @@
         queries.fetchSocial(dataUri, contactId);
         queries.fetchRawContacts(contactId, dataId, rawContactId);
 
-        ContactLoader.Result contact = assertLoadContact(lookupWithWrongIdUri);
+        ContactDetailLoader.Result contact = assertLoadContact(lookupWithWrongIdUri);
 
         assertEquals(contactId, contact.getId());
         assertEquals(rawContactId, contact.getNameRawContactId());
@@ -381,7 +363,7 @@
         queries.fetchSocial(dataUri, contactId);
         queries.fetchRawContacts(contactId, dataId, rawContactId);
 
-        ContactLoader.Result contact = assertLoadContact(lookupWithWrongIdUri);
+        ContactDetailLoader.Result contact = assertLoadContact(lookupWithWrongIdUri);
 
         assertEquals(contactId, contact.getId());
         assertEquals(rawContactId, contact.getNameRawContactId());
@@ -422,9 +404,9 @@
         queries.fetchHeaderDataNoResult(wrongBaseUri);
         queries.fetchLookupAndIdNoResult(lookupWithWrongIdUri);
 
-        ContactLoader.Result contact = assertLoadContact(lookupWithWrongIdUri);
+        ContactDetailLoader.Result contact = assertLoadContact(lookupWithWrongIdUri);
 
-        assertEquals(ContactLoader.Result.NOT_FOUND, contact);
+        assertEquals(ContactDetailLoader.Result.NOT_FOUND, contact);
 
         mContactsProvider.verify();
     }
diff --git a/tests/src/com/android/contacts/ContactDetailTest.java b/tests/src/com/android/contacts/ContactDetailTest.java
index 926a353..6e93bd9 100644
--- a/tests/src/com/android/contacts/ContactDetailTest.java
+++ b/tests/src/com/android/contacts/ContactDetailTest.java
@@ -3,7 +3,7 @@
 import com.android.contacts.activities.ContactDetailActivity;
 import com.android.contacts.tests.mocks.ContactsMockContext;
 import com.android.contacts.tests.mocks.MockContentProvider;
-import com.android.contacts.views.detail.ContactLoader;
+import com.android.contacts.views.detail.ContactDetailLoader;
 
 import android.content.ContentUris;
 import android.content.Intent;