Delta parceling, valid types bugfix, INSERT parsing.

Made EntityDelta and ValuesDelta directly Parcelable to
pass across configuration changes.  Moved the re-parenting
code to separate EntityDelta.mergeAfter() method.  Fixed
getValidTypes() bug that didn't handle typeOverallMax
in "unlimited" cases.  Wrote INSERT parsing code to merge
incoming extras bundle into a new or existing EntityDelta,
also handles any source constraints.

Initial hook-up of edit UI to persist changed data, only
shows first Entity because not connected to tabs yet.
diff --git a/res/layout/act_edit.xml b/res/layout/act_edit.xml
index fe79160..ab04e25 100644
--- a/res/layout/act_edit.xml
+++ b/res/layout/act_edit.xml
@@ -14,40 +14,50 @@
      limitations under the License.
 -->
 
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<ScrollView
+    xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="fill_parent"
     android:layout_height="fill_parent"
-    android:orientation="vertical"
     android:fillViewport="true">
 
-    <!-- TODO: insert aggregate summary widget -->
-    <!-- TODO: insert contact tab widget -->
-
-    <ScrollView
-        android:id="@android:id/tabcontent"
-        android:layout_width="fill_parent"
-        android:layout_height="fill_parent"
-        android:layout_weight="1"
-        android:fillViewport="true" />
-
     <LinearLayout
         android:layout_width="fill_parent"
-        android:layout_height="wrap_content"
-        android:orientation="horizontal"
-        style="@android:style/ButtonBar">
+        android:layout_height="fill_parent"
+        android:orientation="vertical"
+        android:fillViewport="true">
 
-        <Button android:id="@+id/btn_done"
-            android:layout_width="0dip"
-            android:layout_height="wrap_content"
-            android:layout_weight="1"
-            android:text="@string/menu_done" />
+        <!-- TODO: insert aggregate summary widget -->
+        <!-- TODO: insert contact tab widget -->
 
-        <Button android:id="@+id/btn_discard"
-            android:layout_width="0dip"
-            android:layout_height="wrap_content"
+        <FrameLayout
+            android:id="@android:id/tabcontent"
+            android:layout_width="fill_parent"
+            android:layout_height="fill_parent"
             android:layout_weight="1"
-            android:text="@string/menu_doNotSave" />
+            android:fillViewport="true" />
+
+        <LinearLayout
+            android:layout_width="fill_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>
 
-</LinearLayout>
+</ScrollView>
diff --git a/src/com/android/contacts/model/EntityDelta.java b/src/com/android/contacts/model/EntityDelta.java
index 09e2e98..540cd0b 100644
--- a/src/com/android/contacts/model/EntityDelta.java
+++ b/src/com/android/contacts/model/EntityDelta.java
@@ -23,6 +23,7 @@
 import android.content.Entity.NamedContentValues;
 import android.net.Uri;
 import android.os.Parcel;
+import android.os.Parcelable;
 import android.provider.BaseColumns;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.RawContacts;
@@ -46,7 +47,7 @@
  * rows are missing from the new {@link Entity}, we know the original data must
  * be deleted, but to preserve the user modifications we treat as an insert.
  */
-public class EntityDelta {
+public class EntityDelta implements Parcelable {
     // TODO: optimize by using contentvalues pool, since we allocate so many of them
 
     /**
@@ -83,6 +84,34 @@
         return entity;
     }
 
+    /**
+     * Merge the "after" values from the given {@link EntityDelta} onto the
+     * "before" state represented by this {@link EntityDelta}, discarding any
+     * existing "after" states. This is typically used when re-parenting changes
+     * onto an updated {@link Entity}.
+     */
+    public void mergeAfter(EntityDelta remote) {
+        // Always take after values from new state
+        this.mValues.mAfter = remote.mValues.mAfter;
+
+        // Find matching local entry for each remote values, or create
+        for (ArrayList<ValuesDelta> mimeEntries : remote.mEntries.values()) {
+            for (ValuesDelta remoteEntry : mimeEntries) {
+                final Long childId = remoteEntry.getId();
+
+                ValuesDelta localEntry = this.getEntry(childId);
+                if (localEntry == null) {
+                    // Is "insert", or "before" record is missing, so now "insert"
+                    localEntry = ValuesDelta.fromAfter(remoteEntry.mAfter);
+                    this.addEntry(localEntry);
+                } else {
+                    // Existing entry "update"
+                    localEntry.mAfter = remoteEntry.mAfter;
+                }
+            }
+        }
+    }
+
     public ValuesDelta getValues() {
         return mValues;
     }
@@ -91,9 +120,12 @@
      * Get the {@link ValuesDelta} child marked as {@link Data#IS_PRIMARY}.
      */
     public ValuesDelta getPrimaryEntry(String mimeType) {
+        final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false);
+
         // TODO: handle the case where the caller must have a non-null value,
         // for example inserting a displayname automatically
-        final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false);
+        if (mimeEntries == null) return null;
+
         for (ValuesDelta entry : mimeEntries) {
             if (entry.isPrimary()) {
                 return entry;
@@ -129,9 +161,7 @@
     }
 
     /**
-     * Find the {@link ValuesDelta} that has a specific
-     * {@link BaseColumns#_ID} value, used when {@link #augmentFrom(Parcel)} is
-     * inflating a modified state.
+     * Find entry with the given {@link BaseColumns#_ID} value.
      */
     public ValuesDelta getEntry(Long childId) {
         if (childId == null) {
@@ -165,71 +195,6 @@
         return count;
     }
 
-    private static final int MODE_CONTINUE = 1;
-    private static final int MODE_DONE = 2;
-
-    /**
-     * Read a set of modifying actions from the given {@link Parcel}, which
-     * expects the format written by {@link #augmentTo(Parcel)}. This expects
-     * that we already have a base {@link Entity} that we are applying over.
-     */
-    public void augmentFrom(Parcel parcel) {
-        {
-            final ContentValues after = (ContentValues)parcel.readValue(null);
-            if (mValues == null) {
-                // Entity didn't exist before, so "insert"
-                mValues = ValuesDelta.fromAfter(after);
-            } else {
-                // Existing entity "update"
-                mValues.mAfter = after;
-            }
-        }
-
-        // Read in packaged children until finished
-        int mode = parcel.readInt();
-        while (mode == MODE_CONTINUE) {
-            final Long childId = readLong(parcel);
-            final ContentValues after = (ContentValues)parcel.readValue(null);
-
-            ValuesDelta entry = getEntry(childId);
-            if (entry == null) {
-                // Is "insert", or "before" record is missing, so now "insert"
-                entry = ValuesDelta.fromAfter(after);
-                addEntry(entry);
-            } else {
-                // Existing entry "update"
-                entry.mAfter = after;
-            }
-
-            mode = parcel.readInt();
-        }
-    }
-
-    /**
-     * Store all modifying actions into the given {@link Parcel}.
-     */
-    public void augmentTo(Parcel parcel) {
-        parcel.writeValue(mValues.mAfter);
-
-        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
-            for (ValuesDelta child : mimeEntries) {
-                parcel.writeInt(MODE_CONTINUE);
-                writeLong(parcel, child.getId());
-                parcel.writeValue(child.mAfter);
-            }
-        }
-        parcel.writeInt(MODE_DONE);
-    }
-
-    private void writeLong(Parcel parcel, Long value) {
-        parcel.writeLong(value == null ? -1 : value);
-    }
-
-    private Long readLong(Parcel parcel) {
-        final long value = parcel.readLong();
-        return value == -1 ? null : value;
-    }
-
     @Override
     public boolean equals(Object object) {
         if (object instanceof EntityDelta) {
@@ -326,23 +291,63 @@
 
         // If any operations, assert that version is identical so we bail if changed
         if (diff.size() > 0 && beforeVersion != null && beforeId != null) {
-            builder = ContentProviderOperation.newCountQuery(RawContacts.CONTENT_URI);
-            builder.withSelection(RawContacts._ID + "=" + beforeId + " AND " + RawContacts.VERSION
-                    + "=" + beforeVersion, null);
-            builder.withExpectedCount(1);
-            // Sneak version check at beginning of list
-            diff.add(0, builder.build());
+            // TODO: re-enable version enforcement once we have COUNT(*) or ASSERT
+//            builder = ContentProviderOperation.newCountQuery(RawContacts.CONTENT_URI);
+//            builder.withSelection(RawContacts._ID + "=" + beforeId + " AND " + RawContacts.VERSION
+//                    + "=" + beforeVersion, null);
+//            builder.withExpectedCount(1);
+//            // Sneak version check at beginning of list
+//            diff.add(0, builder.build());
         }
 
         return diff;
     }
 
+    /** {@inheritDoc} */
+    public int describeContents() {
+        // Nothing special about this parcel
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    public void writeToParcel(Parcel dest, int flags) {
+        final int size = this.getEntryCount(false);
+        dest.writeInt(size);
+        dest.writeParcelable(mValues, flags);
+        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+            for (ValuesDelta child : mimeEntries) {
+                dest.writeParcelable(child, flags);
+            }
+        }
+    }
+
+    public void readFromParcel(Parcel source) {
+        final int size = source.readInt();
+        mValues = source.<ValuesDelta> readParcelable(null);
+        for (int i = 0; i < size; i++) {
+            final ValuesDelta child = source.<ValuesDelta> readParcelable(null);
+            this.addEntry(child);
+        }
+    }
+
+    public static final Parcelable.Creator<EntityDelta> CREATOR = new Parcelable.Creator<EntityDelta>() {
+        public EntityDelta createFromParcel(Parcel in) {
+            final EntityDelta state = new EntityDelta();
+            state.readFromParcel(in);
+            return state;
+        }
+
+        public EntityDelta[] newArray(int size) {
+            return new EntityDelta[size];
+        }
+    };
+
     /**
      * Type of {@link ContentValues} that maintains both an original state and a
      * modified version of that state. This allows us to build insert, update,
      * or delete operations based on a "before" {@link Entity} snapshot.
      */
-    public static class ValuesDelta {
+    public static class ValuesDelta implements Parcelable {
         private ContentValues mBefore;
         private ContentValues mAfter;
         private String mIdTable = "data";
@@ -402,7 +407,7 @@
         }
 
         public void setIdColumn(String idTable, String idColumn) {
-            // TODO: remove idTable when we've fixed contentprovider
+            // TODO: remove idTable when contentprovider _id collisions fixed
             mIdTable = idTable;
             mIdColumn = idColumn;
         }
@@ -554,5 +559,38 @@
             }
             return builder;
         }
+
+        /** {@inheritDoc} */
+        public int describeContents() {
+            // Nothing special about this parcel
+            return 0;
+        }
+
+        /** {@inheritDoc} */
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeParcelable(mBefore, flags);
+            dest.writeParcelable(mAfter, flags);
+            dest.writeString(mIdTable);
+            dest.writeString(mIdColumn);
+        }
+
+        public void readFromParcel(Parcel source) {
+            mBefore = source.<ContentValues> readParcelable(null);
+            mAfter = source.<ContentValues> readParcelable(null);
+            mIdTable = source.readString();
+            mIdColumn = source.readString();
+        }
+
+        public static final Parcelable.Creator<ValuesDelta> CREATOR = new Parcelable.Creator<ValuesDelta>() {
+            public ValuesDelta createFromParcel(Parcel in) {
+                final ValuesDelta values = new ValuesDelta();
+                values.readFromParcel(in);
+                return values;
+            }
+
+            public ValuesDelta[] newArray(int size) {
+                return new ValuesDelta[size];
+            }
+        };
     }
 }
diff --git a/src/com/android/contacts/model/EntityModifier.java b/src/com/android/contacts/model/EntityModifier.java
index 5dd2880..f0d0017 100644
--- a/src/com/android/contacts/model/EntityModifier.java
+++ b/src/com/android/contacts/model/EntityModifier.java
@@ -16,12 +16,23 @@
 
 package com.android.contacts.model;
 
-import com.android.contacts.model.EntityDelta.ValuesDelta;
 import com.android.contacts.model.ContactsSource.DataKind;
 import com.android.contacts.model.ContactsSource.EditType;
+import com.android.contacts.model.EntityDelta.ValuesDelta;
 
 import android.content.ContentValues;
+import android.os.Bundle;
 import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Intents;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.Intents.Insert;
+import android.text.TextUtils;
+import android.util.Log;
 import android.util.SparseIntArray;
 
 import java.util.ArrayList;
@@ -47,7 +58,11 @@
     }
 
     public static boolean hasValidTypes(EntityDelta state, DataKind kind) {
-        return (getValidTypes(state, kind).size() > 0);
+        if (EntityModifier.hasEditTypes(kind)) {
+            return (getValidTypes(state, kind).size() > 0);
+        } else {
+            return true;
+        }
     }
 
     /**
@@ -102,11 +117,13 @@
         // Build list of valid types
         final int overallCount = typeCount.get(FREQUENCY_TOTAL);
         for (EditType type : kind.typeList) {
-            final boolean validUnlimited = (type.specificMax == -1 && overallCount < kind.typeOverallMax);
-            final boolean validSpecific = (typeCount.get(type.rawValue) < type.specificMax);
+            final boolean validOverall = (kind.typeOverallMax == -1 ? true
+                    : overallCount < kind.typeOverallMax);
+            final boolean validSpecific = (type.specificMax == -1 ? true : typeCount
+                    .get(type.rawValue) < type.specificMax);
             final boolean validSecondary = (includeSecondary ? true : !type.secondary);
             final boolean forcedInclude = type.equals(forceInclude);
-            if (forcedInclude || (validSecondary && (validUnlimited || validSpecific))) {
+            if (forcedInclude || (validOverall && validSpecific && validSecondary)) {
                 // Type is valid when no limit, under limit, or forced include
                 validTypes.add(type);
             }
@@ -183,7 +200,7 @@
      * exist, we pick the last valid option.
      */
     public static EditType getBestValidType(EntityDelta state, DataKind kind,
-            boolean includeSecondary) {
+            boolean includeSecondary, int exactValue) {
         // Shortcut when no types
         if (kind.typeColumn == null) return null;
 
@@ -202,6 +219,11 @@
             final EditType type = iterator.next();
             final int count = typeCount.get(type.rawValue);
 
+            if (count == exactValue) {
+                // Found exact value match
+                return type;
+            }
+
             if (count > 0) {
                 // Type already appears, so don't consider
                 iterator.remove();
@@ -218,24 +240,27 @@
 
     /**
      * Insert a new child of kind {@link DataKind} into the given
-     * {@link EntityDelta}. Tries using the best {@link EditType} found
-     * using {@link #getBestValidType(EntityDelta, DataKind, boolean)}.
+     * {@link EntityDelta}. Tries using the best {@link EditType} found using
+     * {@link #getBestValidType(EntityDelta, DataKind, boolean, int)}.
      */
-    public static void insertChild(EntityDelta state, DataKind kind) {
+    public static ValuesDelta insertChild(EntityDelta state, DataKind kind) {
         // First try finding a valid primary
-        EditType bestType = getBestValidType(state, kind, false);
+        EditType bestType = getBestValidType(state, kind, false, Integer.MIN_VALUE);
         if (bestType == null) {
             // No valid primary found, so expand search to secondary
-            bestType = getBestValidType(state, kind, true);
+            bestType = getBestValidType(state, kind, true, Integer.MIN_VALUE);
         }
-        insertChild(state, kind, bestType);
+        return insertChild(state, kind, bestType);
     }
 
     /**
      * Insert a new child of kind {@link DataKind} into the given
      * {@link EntityDelta}, marked with the given {@link EditType}.
      */
-    public static void insertChild(EntityDelta state, DataKind kind, EditType type) {
+    public static ValuesDelta insertChild(EntityDelta state, DataKind kind, EditType type) {
+        // Bail early if invalid kind
+        if (kind == null) return null;
+
         final ContentValues after = new ContentValues();
 
         // Our parent CONTACT_ID is provided later
@@ -251,7 +276,98 @@
             after.put(kind.typeColumn, type.rawValue);
         }
 
-        state.addEntry(ValuesDelta.fromAfter(after));
+        final ValuesDelta child = ValuesDelta.fromAfter(after);
+        state.addEntry(child);
+        return child;
     }
 
+    /**
+     * Parse the given {@link Bundle} into the given {@link EntityDelta} state,
+     * assuming the extras defined through {@link Intents}.
+     */
+    public static void parseExtras(EntityDelta state, Bundle extras) {
+        final String accountType = state.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+        final ContactsSource source = Sources.getInstance().getSourceForType(accountType);
+
+        {
+            // StructuredName
+            final DataKind kind = source.getKindForMimetype(StructuredName.CONTENT_ITEM_TYPE);
+            final String name = extras.getString(Insert.NAME);
+            final String phoneticName = extras.getString(Insert.PHONETIC_NAME);
+            if (kind != null && (TextUtils.isGraphic(name) || TextUtils.isGraphic(phoneticName))) {
+                // TODO: handle the case where name already exists and limited to one
+                // TODO: represent phonetic name as structured fields
+                final ValuesDelta child = EntityModifier.insertChild(state, kind, null);
+                child.put(StructuredName.DISPLAY_NAME, name);
+                child.put(StructuredName.PHONETIC_GIVEN_NAME, phoneticName);
+            }
+        }
+
+        {
+            // StructuredPostal
+            final DataKind kind = source.getKindForMimetype(StructuredPostal.CONTENT_ITEM_TYPE);
+            parseExtras(state, kind, extras, Insert.POSTAL_TYPE, Insert.POSTAL,
+                    StructuredPostal.FORMATTED_ADDRESS);
+        }
+
+        {
+            // Phone
+            final DataKind kind = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+            parseExtras(state, kind, extras, Insert.PHONE_TYPE, Insert.PHONE, Phone.NUMBER);
+            parseExtras(state, kind, extras, Insert.SECONDARY_PHONE_TYPE, Insert.SECONDARY_PHONE,
+                    Phone.NUMBER);
+            parseExtras(state, kind, extras, Insert.TERTIARY_PHONE_TYPE, Insert.TERTIARY_PHONE,
+                    Phone.NUMBER);
+        }
+
+        {
+            // Email
+            final DataKind kind = source.getKindForMimetype(Email.CONTENT_ITEM_TYPE);
+            parseExtras(state, kind, extras, Insert.EMAIL_TYPE, Insert.PHONE, Email.DATA);
+            parseExtras(state, kind, extras, Insert.SECONDARY_EMAIL_TYPE, Insert.SECONDARY_EMAIL,
+                    Email.DATA);
+            parseExtras(state, kind, extras, Insert.TERTIARY_EMAIL_TYPE, Insert.TERTIARY_EMAIL,
+                    Email.DATA);
+        }
+
+        {
+            // Im
+            // TODO: handle decodeImProtocol for legacy reasons
+            final DataKind kind = source.getKindForMimetype(Im.CONTENT_ITEM_TYPE);
+            parseExtras(state, kind, extras, Insert.IM_PROTOCOL, Insert.IM_HANDLE, Im.DATA);
+        }
+    }
+
+    /**
+     * Parse a specific entry from the given {@link Bundle} and insert into the
+     * given {@link EntityDelta}. Silently skips the insert when missing value
+     * or no valid {@link EditType} found.
+     *
+     * @param typeExtra {@link Bundle} key that holds the incoming
+     *            {@link EditType#rawValue} value.
+     * @param valueExtra {@link Bundle} key that holds the incoming value.
+     * @param valueColumn Column to write value into {@link ValuesDelta}.
+     */
+    public static void parseExtras(EntityDelta state, DataKind kind, Bundle extras,
+            String typeExtra, String valueExtra, String valueColumn) {
+        final String value = extras.getString(valueExtra);
+
+        // Bail when can't insert type, or value missing
+        final boolean canInsert = EntityModifier.canInsert(state, kind);
+        final boolean validValue = TextUtils.isGraphic(value);
+        if (!validValue || !canInsert) return;
+
+        // Find exact type, or otherwise best type
+        final int typeValue = extras.getInt(typeExtra, Integer.MIN_VALUE);
+        final EditType editType = EntityModifier.getBestValidType(state, kind, true, typeValue);
+
+        // Create data row and fill with value
+        final ValuesDelta child = EntityModifier.insertChild(state, kind, editType);
+        child.put(valueColumn, value);
+
+        if (editType != null && editType.customColumn != null) {
+            // Write down label when custom type picked
+            child.put(editType.customColumn, extras.getString(typeExtra));
+        }
+    }
 }
diff --git a/src/com/android/contacts/model/Sources.java b/src/com/android/contacts/model/Sources.java
index e77a7dd..f0a2840 100644
--- a/src/com/android/contacts/model/Sources.java
+++ b/src/com/android/contacts/model/Sources.java
@@ -63,7 +63,7 @@
     }
 
     public static final String ACCOUNT_TYPE_GOOGLE = "com.google.GAIA";
-    public static final String ACCOUNT_TYPE_EXCHANGE = "vnd.exchange";
+    public static final String ACCOUNT_TYPE_EXCHANGE = "com.android.exchange";
 
     private HashMap<String, ContactsSource> mSources = new HashMap<String, ContactsSource>();
 
@@ -76,7 +76,7 @@
      * Find the {@link ContactsSource} for the given
      * {@link Contacts#ACCOUNT_TYPE}.
      */
-    public ContactsSource getKindsForAccountType(String accountType) {
+    public ContactsSource getSourceForType(String accountType) {
         return mSources.get(accountType);
     }
 
diff --git a/src/com/android/contacts/ui/EditContactActivity.java b/src/com/android/contacts/ui/EditContactActivity.java
index 94a67bf..f173172 100644
--- a/src/com/android/contacts/ui/EditContactActivity.java
+++ b/src/com/android/contacts/ui/EditContactActivity.java
@@ -17,9 +17,10 @@
 package com.android.contacts.ui;
 
 import com.android.contacts.R;
-import com.android.contacts.model.EntityDelta;
 import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.EntityDelta;
 import com.android.contacts.model.Sources;
+import com.android.contacts.model.EntityDelta.ValuesDelta;
 import com.android.contacts.ui.widget.ContactEditorView;
 
 import android.app.Activity;
@@ -27,6 +28,7 @@
 import android.content.ContentProviderOperation;
 import android.content.ContentResolver;
 import android.content.ContentUris;
+import android.content.ContentValues;
 import android.content.Context;
 import android.content.Entity;
 import android.content.EntityIterator;
@@ -36,13 +38,13 @@
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.provider.ContactsContract;
+import android.provider.ContactsContract.RawContacts;
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
-import android.view.ViewGroup;
 
 import java.util.ArrayList;
 
@@ -52,8 +54,6 @@
 public final class EditContactActivity extends Activity implements View.OnClickListener,
         View.OnFocusChangeListener {
 
-    // TODO: with new augmentedentity approach, turn insert and update cases into same action
-
     private static final String TAG = "EditContactActivity";
 
     /** The launch code when picking a photo and the raw data is returned */
@@ -67,26 +67,13 @@
     public static final int MENU_ITEM_REVERT = 2;
     public static final int MENU_ITEM_PHOTO = 6;
 
-
-    private LayoutInflater mInflater;
-    private ViewGroup mContentView;
-
-//    private MenuItem mPhotoMenuItem;
-//    private boolean mPhotoPresent = false;
-
-    /** Flag marking this contact as changed, meaning we should write changes back. */
-//    private boolean mContactChanged = false;
-
-    private Uri mUri;
-    private ArrayList<EntityDelta> mEntities = new ArrayList<EntityDelta>();
-
-    private ContentResolver mResolver;
+    private View mTabContent;
     private ContactEditorView mEditor;
 
-    private ViewGroup mTabContent;
+    private Uri mUri;
+    private Sources mSources;
 
-    // we edit an aggregate, which has several entities
-    // we query and build AugmentedEntities, which is what ContactEditorView expects
+    private ArrayList<EntityDelta> mEntities = new ArrayList<EntityDelta>();
 
 
 
@@ -95,233 +82,135 @@
         super.onCreate(icicle);
 
         final Context context = this;
+        final LayoutInflater inflater = this.getLayoutInflater();
 
-        mInflater = getLayoutInflater();
-        mResolver = getContentResolver();
+        setContentView(R.layout.act_edit);
 
-        mContentView = (ViewGroup)mInflater.inflate(R.layout.act_edit, null);
-        mTabContent = (ViewGroup)mContentView.findViewById(android.R.id.tabcontent);
+        mTabContent = this.findViewById(android.R.id.tabcontent);
 
-        setContentView(mContentView);
+        mEditor = new ContactEditorView(context);
+        mEditor.swapWith(mTabContent);
 
-        // Setup floating buttons
         findViewById(R.id.btn_done).setOnClickListener(this);
         findViewById(R.id.btn_discard).setOnClickListener(this);
 
-
-
         final Intent intent = getIntent();
         final String action = intent.getAction();
         final Bundle extras = intent.getExtras();
 
         mUri = intent.getData();
+        mSources = Sources.getInstance();
 
-        // TODO: read all contacts part of this aggregate and hook into tabs
+        if (Intent.ACTION_EDIT.equals(action) && icicle == null) {
+            // Read initial state from database
+            readEntities();
+            rebuildTabs();
+        }
+    }
 
-        // Resolve the intent
-        if (Intent.ACTION_EDIT.equals(action) && mUri != null) {
+    private static final String KEY_DELTAS = "deltas";
 
-            try {
-                final long aggId = ContentUris.parseId(mUri);
-                final EntityIterator iterator = mResolver.queryEntities(
-                        ContactsContract.RawContacts.CONTENT_URI,
-                        ContactsContract.RawContacts.CONTACT_ID + "=" + aggId, null, null);
-                while (iterator.hasNext()) {
-                    final Entity before = iterator.next();
-                    final EntityDelta entity = EntityDelta.fromBefore(before);
+    @Override
+    protected void onSaveInstanceState(Bundle outState) {
+        // Store entities with modifications
+        outState.putParcelableArrayList(KEY_DELTAS, mEntities);
 
-                    mEntities.add(entity);
+        super.onSaveInstanceState(outState);
+    }
 
-                    Log.d(TAG, "Loaded entity...");
-                }
-                iterator.close();
-            } catch (RemoteException e) {
-                Log.d(TAG, "Problem reading aggregate", e);
-            }
+    @Override
+    protected void onRestoreInstanceState(Bundle savedInstanceState) {
 
-//            if (icicle == null) {
-//                // Build the entries & views
-//                buildEntriesForEdit(extras);
-//                buildViews();
-//            }
-            setTitle(R.string.editContact_title_edit);
-        } else if (Intent.ACTION_INSERT.equals(action)) {
-//            if (icicle == null) {
-//                // Build the entries & views
-//                buildEntriesForInsert(extras);
-//                buildViews();
-//            }
-//            setTitle(R.string.editContact_title_insert);
-//            mState = STATE_INSERT;
+        // Read and apply modifications from instance
+        mEntities = savedInstanceState.<EntityDelta> getParcelableArrayList(KEY_DELTAS);
+        rebuildTabs();
+
+        // Restore selected tab and any focus
+        super.onRestoreInstanceState(savedInstanceState);
+    }
+
+    @Override
+    protected void onPostCreate(Bundle savedInstanceState) {
+        super.onPostCreate(savedInstanceState);
+
+        final String action = getIntent().getAction();
+        if (Intent.ACTION_INSERT.equals(action)) {
+            // TODO: show account disambig dialog before creating
+
+            final ContentValues values = new ContentValues();
+            values.put(RawContacts.ACCOUNT_TYPE, Sources.ACCOUNT_TYPE_GOOGLE);
+
+            final EntityDelta insert = new EntityDelta(ValuesDelta.fromAfter(values));
+            mEntities.add(insert);
+
         }
 
-//        if (mState == STATE_UNKNOWN) {
-//            Log.e(TAG, "Cannot resolve intent: " + intent);
-//            finish();
-//            return;
-//        }
+        // TODO: if insert, handle account disambig if not already done
+    }
 
-        mEditor = new ContactEditorView(context);
+    protected void readEntities() {
+        // TODO: handle saving the previous values before replacing, in some cases
+        try {
+            final ContentResolver resolver = this.getContentResolver();
+            final long aggId = ContentUris.parseId(mUri);
+            final EntityIterator iterator = resolver.queryEntities(
+                    ContactsContract.RawContacts.CONTENT_URI,
+                    ContactsContract.RawContacts.CONTACT_ID + "=" + aggId, null, null);
+            while (iterator.hasNext()) {
+                final Entity before = iterator.next();
+                final EntityDelta entity = EntityDelta.fromBefore(before);
+                mEntities.add(entity);
+            }
+            iterator.close();
+        } catch (RemoteException e) {
+            Log.d(TAG, "Problem reading aggregate", e);
+        }
+    }
 
-        mTabContent.removeAllViews();
-        mTabContent.addView(mEditor.getView());
+    protected void rebuildTabs() {
+        // TODO: hook up to tabs
+        showEntity(0);
+    }
 
-//        final ContactsSource source = Sources.getInstance().getKindsForAccountType(
-//                Sources.ACCOUNT_TYPE_GOOGLE);
-        final ContactsSource source = Sources.getInstance().getKindsForAccountType(
-                Sources.ACCOUNT_TYPE_EXCHANGE);
-        mEditor.setState(mEntities.get(0), source);
+    protected void showEntity(int index) {
+        // Find entity and source for selected tab
+        final EntityDelta entity = mEntities.get(index);
+        final String accountType = entity.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
 
+        ContactsSource source = mSources.getSourceForType(accountType);
+        if (source == null) {
+            // TODO: remove and place "read only" placard when missing
+            source = mSources.getSourceForType(Sources.ACCOUNT_TYPE_GOOGLE);
+        }
 
+        // Assign editor state based on entity and source
+        mEditor.setState(entity, source);
+    }
+
+    public void onTabChanged(String tabId) {
+        // Tag is really an array index
+        final int index = Integer.parseInt(tabId);
+        showEntity(index);
     }
 
 
-    // TODO: build entity from incoming intent
-    // TODO: build entity from "new" action
-
-
-//    private void addFromExtras(Bundle extras, Uri phonesUri, Uri methodsUri) {
-//        EditEntry entry;
-//
-//        // Read the name from the bundle
-//        CharSequence name = extras.getCharSequence(Insert.NAME);
-//        if (name != null && TextUtils.isGraphic(name)) {
-//            mNameView.setText(name);
-//        }
-//
-//        // Read the phonetic name from the bundle
-//        CharSequence phoneticName = extras.getCharSequence(Insert.PHONETIC_NAME);
-//        if (!TextUtils.isEmpty(phoneticName)) {
-//            mPhoneticNameView.setText(phoneticName);
-//        }
-//
-//        // StructuredPostal entries from extras
-//        CharSequence postal = extras.getCharSequence(Insert.POSTAL);
-//        int postalType = extras.getInt(Insert.POSTAL_TYPE, INVALID_TYPE);
-//        if (!TextUtils.isEmpty(postal) && postalType == INVALID_TYPE) {
-//            postalType = DEFAULT_POSTAL_TYPE;
-//        }
-//
-//        if (postalType != INVALID_TYPE) {
-//            entry = EditEntry.newPostalEntry(this, null, postalType, postal.toString(),
-//                    methodsUri, 0);
-//            entry.isPrimary = extras.getBoolean(Insert.POSTAL_ISPRIMARY);
-//            mPostalEntries.add(entry);
-//        }
-//
-//        // Email entries from extras
-//        addEmailFromExtras(extras, methodsUri, Insert.EMAIL, Insert.EMAIL_TYPE,
-//                Insert.EMAIL_ISPRIMARY);
-//        addEmailFromExtras(extras, methodsUri, Insert.SECONDARY_EMAIL, Insert.SECONDARY_EMAIL_TYPE,
-//                null);
-//        addEmailFromExtras(extras, methodsUri, Insert.TERTIARY_EMAIL, Insert.TERTIARY_EMAIL_TYPE,
-//                null);
-//
-//        // Phone entries from extras
-//        addPhoneFromExtras(extras, phonesUri, Insert.PHONE, Insert.PHONE_TYPE,
-//                Insert.PHONE_ISPRIMARY);
-//        addPhoneFromExtras(extras, phonesUri, Insert.SECONDARY_PHONE, Insert.SECONDARY_PHONE_TYPE,
-//                null);
-//        addPhoneFromExtras(extras, phonesUri, Insert.TERTIARY_PHONE, Insert.TERTIARY_PHONE_TYPE,
-//                null);
-//
-//        // IM entries from extras
-//        CharSequence imHandle = extras.getCharSequence(Insert.IM_HANDLE);
-//        CharSequence imProtocol = extras.getCharSequence(Insert.IM_PROTOCOL);
-//
-//        if (imHandle != null && imProtocol != null) {
-//            Object protocolObj = ContactMethods.decodeImProtocol(imProtocol.toString());
-//            if (protocolObj instanceof Number) {
-//                int protocol = ((Number)F protocolObj).intValue();
-//                entry = EditEntry.newImEntry(this,
-//                        getLabelsForKind(this, Contacts.KIND_IM)[protocol], protocol,
-//                        imHandle.toString(), methodsUri, 0);
-//            } else {
-//                entry = EditEntry.newImEntry(this, protocolObj.toString(), -1, imHandle.toString(),
-//                        methodsUri, 0);
-//            }
-//            entry.isPrimary = extras.getBoolean(Insert.IM_ISPRIMARY);
-//            mImEntries.add(entry);
-//        }
-//    }
-//
-//    private void addEmailFromExtras(Bundle extras, Uri methodsUri, String emailField,
-//            String typeField, String primaryField) {
-//        CharSequence email = extras.getCharSequence(emailField);
-//
-//        // Correctly handle String in typeField as TYPE_CUSTOM
-//        int emailType = INVALID_TYPE;
-//        String customLabel = null;
-//        if(extras.get(typeField) instanceof String) {
-//            emailType = ContactsContract.TYPE_CUSTOM;
-//            customLabel = extras.getString(typeField);
-//        } else {
-//            emailType = extras.getInt(typeField, INVALID_TYPE);
-//        }
-//
-//        if (!TextUtils.isEmpty(email) && emailType == INVALID_TYPE) {
-//            emailType = DEFAULT_EMAIL_TYPE;
-//            mPrimaryEmailAdded = true;
-//        }
-//
-//        if (emailType != INVALID_TYPE) {
-//            EditEntry entry = EditEntry.newEmailEntry(this, customLabel, emailType, email.toString(),
-//                    methodsUri, 0);
-//            entry.isPrimary = (primaryField == null) ? false : extras.getBoolean(primaryField);
-//            mEmailEntries.add(entry);
-//
-//            // Keep track of which primary types have been added
-//            if (entry.isPrimary) {
-//                mPrimaryEmailAdded = true;
-//            }
-//        }
-//    }
-//
-//    private void addPhoneFromExtras(Bundle extras, Uri phonesUri, String phoneField,
-//            String typeField, String primaryField) {
-//        CharSequence phoneNumber = extras.getCharSequence(phoneField);
-//
-//        // Correctly handle String in typeField as TYPE_CUSTOM
-//        int phoneType = INVALID_TYPE;
-//        String customLabel = null;
-//        if(extras.get(typeField) instanceof String) {
-//            phoneType = Phone.TYPE_CUSTOM;
-//            customLabel = extras.getString(typeField);
-//        } else {
-//            phoneType = extras.getInt(typeField, INVALID_TYPE);
-//        }
-//
-//        if (!TextUtils.isEmpty(phoneNumber) && phoneType == INVALID_TYPE) {
-//            phoneType = DEFAULT_PHONE_TYPE;
-//        }
-//
-//        if (phoneType != INVALID_TYPE) {
-//            EditEntry entry = EditEntry.newPhoneEntry(this, customLabel, phoneType,
-//                    phoneNumber.toString(), phonesUri, 0);
-//            entry.isPrimary = (primaryField == null) ? false : extras.getBoolean(primaryField);
-//            mPhoneEntries.add(entry);
-//
-//            // Keep track of which primary types have been added
-//            if (phoneType == Phone.TYPE_MOBILE) {
-//                mMobilePhoneAdded = true;
-//            }
-//        }
-//    }
-//    */
+    public View createTabContent(String tag) {
+        // Content is identical for all tabs
+        return mEditor.getView();
+    }
 
 
 
-    public void onClick(View v) {
-        switch (v.getId()) {
-
-            case R.id.btn_done:
+    public void onClick(View view) {
+        switch (view.getId()) {
+            case R.id.btn_done: {
                 doSaveAction();
                 break;
-
-            case R.id.btn_discard:
+            }
+            case R.id.btn_discard: {
                 doRevertAction();
                 break;
-
+            }
         }
     }
 
@@ -338,119 +227,6 @@
 
 
 
-    @Override
-    protected void onSaveInstanceState(Bundle outState) {
-
-        // TODO: store down uri, selected contactid, and pile of augmentedstates
-
-        // store down the focused tab and child data  _id (what about storing as index?)
-
-        // tell sections to store their state, which also picks correct field and cursor location
-
-
-
-//
-//        // To store current focus between config changes, follow focus down the
-//        // view tree, keeping track of any parents with EditEntry tags
-//        View focusedChild = mContentView.getFocusedChild();
-//        EditEntry focusedEntry = null;
-//        while (focusedChild != null) {
-//            Object tag = focusedChild.getTag();
-//            if (tag instanceof EditEntry) {
-//                focusedEntry = (EditEntry) tag;
-//            }
-//
-//            // Keep going deeper until child isn't a group
-//            if (focusedChild instanceof ViewGroup) {
-//                View deeperFocus = ((ViewGroup) focusedChild).getFocusedChild();
-//                if (deeperFocus != null) {
-//                    focusedChild = deeperFocus;
-//                } else {
-//                    break;
-//                }
-//            } else {
-//                break;
-//            }
-//        }
-//
-//        if (focusedChild != null) {
-//            int requestFocusId = focusedChild.getId();
-//            int requestCursor = 0;
-//            if (focusedChild instanceof EditText) {
-//                requestCursor = ((EditText) focusedChild).getSelectionStart();
-//            }
-//
-//            // Store focus values in EditEntry if found, otherwise store as
-//            // generic values
-//            if (focusedEntry != null) {
-//                focusedEntry.requestFocusId = requestFocusId;
-//                focusedEntry.requestCursor = requestCursor;
-//            } else {
-//                outState.putInt("requestFocusId", requestFocusId);
-//                outState.putInt("requestCursor", requestCursor);
-//            }
-//        }
-//
-//        outState.putParcelableArrayList("phoneEntries", mPhoneEntries);
-//        outState.putParcelableArrayList("emailEntries", mEmailEntries);
-//        outState.putParcelableArrayList("imEntries", mImEntries);
-//        outState.putParcelableArrayList("postalEntries", mPostalEntries);
-//        outState.putParcelableArrayList("orgEntries", mOrgEntries);
-//        outState.putParcelableArrayList("noteEntries", mNoteEntries);
-//        outState.putParcelableArrayList("otherEntries", mOtherEntries);
-//        outState.putInt("state", mState);
-//        outState.putBoolean("insert", mInsert);
-//        outState.putParcelable("uri", mUri);
-//        outState.putString("name", mNameView.getText().toString());
-//        outState.putParcelable("photo", mPhoto);
-//        outState.putBoolean("photoChanged", mPhotoChanged);
-//        outState.putString("phoneticName", mPhoneticNameView.getText().toString());
-//        outState.putBoolean("contactChanged", mContactChanged);
-    }
-
-    @Override
-    protected void onRestoreInstanceState(Bundle inState) {
-//        mPhoneEntries = inState.getParcelableArrayList("phoneEntries");
-//        mEmailEntries = inState.getParcelableArrayList("emailEntries");
-//        mImEntries = inState.getParcelableArrayList("imEntries");
-//        mPostalEntries = inState.getParcelableArrayList("postalEntries");
-//        mOrgEntries = inState.getParcelableArrayList("orgEntries");
-//        mNoteEntries = inState.getParcelableArrayList("noteEntries");
-//        mOtherEntries = inState.getParcelableArrayList("otherEntries");
-//        setupSections();
-//
-//        mState = inState.getInt("state");
-//        mInsert = inState.getBoolean("insert");
-//        mUri = inState.getParcelable("uri");
-//        mNameView.setText(inState.getString("name"));
-//        mPhoto = inState.getParcelable("photo");
-//        if (mPhoto != null) {
-//            mPhotoImageView.setImageBitmap(mPhoto);
-//            setPhotoPresent(true);
-//        } else {
-//            mPhotoImageView.setImageResource(R.drawable.ic_contact_picture);
-//            setPhotoPresent(false);
-//        }
-//        mPhotoChanged = inState.getBoolean("photoChanged");
-//        mPhoneticNameView.setText(inState.getString("phoneticName"));
-//        mContactChanged = inState.getBoolean("contactChanged");
-//
-//        // Now that everything is restored, build the view
-//        buildViews();
-//
-//        // Try restoring any generally requested focus
-//        int requestFocusId = inState.getInt("requestFocusId", View.NO_ID);
-//        View focusedChild = mContentView.findViewById(requestFocusId);
-//        if (focusedChild != null) {
-//            focusedChild.requestFocus();
-//            if (focusedChild instanceof EditText) {
-//                int requestCursor = inState.getInt("requestCursor", 0);
-//                ((EditText) focusedChild).setSelection(requestCursor);
-//            }
-//        }
-    }
-
-
 
     @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
@@ -575,300 +351,9 @@
         }
 
         this.finish();
-
-
-
-        // Save or create the contact if needed
-//        switch (mState) {
-//            case STATE_EDIT:
-//                save();
-//                break;
-//
-//            /*
-//            case STATE_INSERT:
-//                create();
-//                break;
-//            */
-//
-//            default:
-//                Log.e(TAG, "Unknown state in doSaveOrCreate: " + mState);
-//                break;
-//        }
-        finish();
     }
 
 
-    /**
-     * Save the various fields to the existing contact.
-     */
-    private void save() {
-//        ContentValues values = new ContentValues();
-//        String data;
-//        int numValues = 0;
-//
-//        // Handle the name and send to voicemail specially
-//        final String name = mNameView.getText().toString();
-//        if (name != null && TextUtils.isGraphic(name)) {
-//            numValues++;
-//        }
-//
-//        values.put(StructuredName.DISPLAY_NAME, name);
-//        /*
-//        values.put(People.PHONETIC_NAME, mPhoneticNameView.getText().toString());
-//        */
-//        mResolver.update(mStructuredNameUri, values, null, null);
-//
-//        // This will go down in for loop somewhere
-//        if (mPhotoChanged) {
-//            // Only write the photo if it's changed, since we don't initially load mPhoto
-//            values.clear();
-//            if (mPhoto != null) {
-//                ByteArrayOutputStream stream = new ByteArrayOutputStream();
-//                mPhoto.compress(Bitmap.CompressFormat.JPEG, 75, stream);
-//                values.put(Photo.PHOTO, stream.toByteArray());
-//                mResolver.update(mPhotoDataUri, values, null, null);
-//            } else {
-//                values.putNull(Photo.PHOTO);
-//                mResolver.update(mPhotoDataUri, values, null, null);
-//            }
-//        }
-//
-//        int entryCount = ContactEntryAdapter.countEntries(mSections, false);
-//        for (int i = 0; i < entryCount; i++) {
-//            EditEntry entry = ContactEntryAdapter.getEntry(mSections, i, false);
-//            data = entry.getData();
-//            boolean empty = data == null || !TextUtils.isGraphic(data);
-//            /*
-//            if (kind == EditEntry.KIND_GROUP) {
-//                if (entry.id != 0) {
-//                    for (int g = 0; g < mGroups.length; g++) {
-//                        long groupId = getGroupId(mResolver, mGroups[g].toString());
-//                        if (mInTheGroup[g]) {
-//                            Contacts.People.addToGroup(mResolver, entry.id, groupId);
-//                            numValues++;
-//                        } else {
-//                            deleteGroupMembership(entry.id, groupId);
-//                        }
-//                    }
-//                }
-//            }
-//            */
-//            if (!empty) {
-//                values.clear();
-//                entry.toValues(values);
-//                if (entry.id != 0) {
-//                    mResolver.update(entry.uri, values, null, null);
-//                } else {
-//                    /* mResolver.insert(entry.uri, values); */
-//                }
-//            } else if (entry.id != 0) {
-//                mResolver.delete(entry.uri, null, null);
-//            }
-//        }
-//
-//        /*
-//        if (numValues == 0) {
-//            // The contact is completely empty, delete it
-//            mResolver.delete(mUri, null, null);
-//            mUri = null;
-//            setResult(RESULT_CANCELED);
-//        } else {
-//            // Add the entry to the my contacts group if it isn't there already
-//            People.addToMyContactsGroup(mResolver, ContentUris.parseId(mUri));
-//            setResult(RESULT_OK, new Intent().setData(mUri));
-//
-//            // Only notify user if we actually changed contact
-//            if (mContactChanged || mPhotoChanged) {
-//                Toast.makeText(this, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
-//            }
-//        }
-//        */
-    }
-
-    /**
-     * Takes the entered data and saves it to a new contact.
-     */
-    /*
-    private void create() {
-        ContentValues values = new ContentValues();
-        String data;
-        int numValues = 0;
-
-        // Create the contact itself
-        final String name = mNameView.getText().toString();
-        if (name != null && TextUtils.isGraphic(name)) {
-            numValues++;
-        }
-        values.put(People.NAME, name);
-        values.put(People.PHONETIC_NAME, mPhoneticNameView.getText().toString());
-
-        // Add the contact to the My Contacts group
-        Uri contactUri = People.createPersonInMyContactsGroup(mResolver, values);
-
-        // Add the contact to the group that is being displayed in the contact list
-        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
-        int displayType = prefs.getInt(ContactsListActivity.PREF_DISPLAY_TYPE,
-                ContactsListActivity.DISPLAY_TYPE_UNKNOWN);
-        if (displayType == ContactsListActivity.DISPLAY_TYPE_USER_GROUP) {
-            String displayGroup = prefs.getString(ContactsListActivity.PREF_DISPLAY_INFO,
-                    null);
-            if (!TextUtils.isEmpty(displayGroup)) {
-                People.addToGroup(mResolver, ContentUris.parseId(contactUri), displayGroup);
-            }
-        } else {
-            // Check to see if we're not syncing everything and if so if My Contacts is synced.
-            // If it isn't then the created contact can end up not in any groups that are
-            // currently synced and end up getting removed from the phone, which is really bad.
-            boolean syncingEverything = !"0".equals(Contacts.Settings.getSetting(mResolver, null,
-                    Contacts.Settings.SYNC_EVERYTHING));
-            if (!syncingEverything) {
-                boolean syncingMyContacts = false;
-                Cursor c = mResolver.query(Groups.CONTENT_URI, new String[] { Groups.SHOULD_SYNC },
-                        Groups.SYSTEM_ID + "=?", new String[] { Groups.GROUP_MY_CONTACTS }, null);
-                if (c != null) {
-                    try {
-                        if (c.moveToFirst()) {
-                            syncingMyContacts = !"0".equals(c.getString(0));
-                        }
-                    } finally {
-                        c.close();
-                    }
-                }
-
-                if (!syncingMyContacts) {
-                    // Not syncing My Contacts, so find a group that is being synced and stick
-                    // the contact in there. We sort the list so at least all contacts
-                    // will appear in the same group.
-                    c = mResolver.query(Groups.CONTENT_URI, new String[] { Groups._ID },
-                            Groups.SHOULD_SYNC + "!=0", null, Groups.DEFAULT_SORT_ORDER);
-                    if (c != null) {
-                        try {
-                            if (c.moveToFirst()) {
-                                People.addToGroup(mResolver, ContentUris.parseId(contactUri),
-                                        c.getLong(0));
-                            }
-                        } finally {
-                            c.close();
-                        }
-                    }
-                }
-            }
-        }
-
-        // Handle the photo
-        if (mPhoto != null) {
-            ByteArrayOutputStream stream = new ByteArrayOutputStream();
-            mPhoto.compress(Bitmap.CompressFormat.JPEG, 75, stream);
-            Contacts.People.setPhotoData(getContentResolver(), contactUri, stream.toByteArray());
-        }
-
-        // Create the contact methods
-        int entryCount = ContactEntryAdapter.countEntries(mSections, false);
-        for (int i = 0; i < entryCount; i++) {
-            EditEntry entry = ContactEntryAdapter.getEntry(mSections, i, false);
-            if (entry.kind == EditEntry.KIND_GROUP) {
-                long contactId = ContentUris.parseId(contactUri);
-                for (int g = 0; g < mGroups.length; g++) {
-                    if (mInTheGroup[g]) {
-                        long groupId = getGroupId(mResolver, mGroups[g].toString());
-                        People.addToGroup(mResolver, contactId, groupId);
-                        numValues++;
-                    }
-                }
-            } else if (entry.kind != EditEntry.KIND_CONTACT) {
-                values.clear();
-                if (entry.toValues(values)) {
-                    // Only create the entry if there is data
-                    entry.uri = mResolver.insert(
-                            Uri.withAppendedPath(contactUri, entry.contentDirectory), values);
-                    entry.id = ContentUris.parseId(entry.uri);
-                }
-            } else {
-                // Update the contact with any straggling data, like notes
-                data = entry.getData();
-                values.clear();
-                if (data != null && TextUtils.isGraphic(data)) {
-                    values.put(entry.column, data);
-                    mResolver.update(contactUri, values, null, null);
-                }
-            }
-        }
-
-        if (numValues == 0) {
-            mResolver.delete(contactUri, null, null);
-            setResult(RESULT_CANCELED);
-        } else {
-            mUri = contactUri;
-            Intent resultIntent = new Intent()
-                    .setData(mUri)
-                    .putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
-            setResult(RESULT_OK, resultIntent);
-            Toast.makeText(this, R.string.contactCreatedToast, Toast.LENGTH_SHORT).show();
-        }
-    }
-    */
-
-
-
-//    /**
-//     * Builds the views for a specific section.
-//     *
-//     * @param layout the container
-//     * @param section the section to build the views for
-//     */
-//    private void buildViewsForSection(final LinearLayout layout, ArrayList<EditEntry> section,
-//            int separatorResource, int sectionType) {
-//
-//        View divider = mInflater.inflate(R.layout.edit_divider, layout, false);
-//        layout.addView(divider);
-//
-//        // Count up undeleted children
-//        int activeChildren = 0;
-//        for (int i = section.size() - 1; i >= 0; i--) {
-//            EditEntry entry = section.get(i);
-//            if (!entry.isDeleted) {
-//                activeChildren++;
-//            }
-//        }
-//
-//        // Build the correct group header based on undeleted children
-//        ViewGroup header;
-//        if (activeChildren == 0) {
-//            header = (ViewGroup) mInflater.inflate(R.layout.edit_separator_alone, layout, false);
-//        } else {
-//            header = (ViewGroup) mInflater.inflate(R.layout.edit_separator, layout, false);
-//        }
-//
-//        // Because we're emulating a ListView, we need to handle focus changes
-//        // with some additional logic.
-//        header.setOnFocusChangeListener(this);
-//
-//        TextView text = (TextView) header.findViewById(R.id.text);
-//        text.setText(getText(separatorResource));
-//
-//        // Force TextView to always default color if we have children.  This makes sure
-//        // we don't change color when parent is pressed.
-//        if (activeChildren > 0) {
-//            ColorStateList stateList = text.getTextColors();
-//            text.setTextColor(stateList.getDefaultColor());
-//        }
-//
-//        View addView = header.findViewById(R.id.separator);
-//        addView.setTag(Integer.valueOf(sectionType));
-//        addView.setOnClickListener(this);
-//
-//        // Build views for the current section
-//        for (EditEntry entry : section) {
-//            entry.activity = this; // this could be null from when the state is restored
-//            if (!entry.isDeleted) {
-//                View view = buildViewForEntry(entry);
-//                header.addView(view);
-//            }
-//        }
-//
-//        layout.addView(header);
-//    }
-
 
 
 
@@ -877,4 +362,6 @@
         // views as they are focused.
         v.setSelected(hasFocus);
     }
+
+
 }
diff --git a/src/com/android/contacts/ui/widget/ContactEditorView.java b/src/com/android/contacts/ui/widget/ContactEditorView.java
index 5e7785a..6e67dd6 100644
--- a/src/com/android/contacts/ui/widget/ContactEditorView.java
+++ b/src/com/android/contacts/ui/widget/ContactEditorView.java
@@ -75,7 +75,10 @@
         mSecondary = (ViewGroup)mContent.findViewById(R.id.sect_secondary);
 
         mPhoto = new PhotoEditor(context);
+        mPhoto.swapWith(mContent, R.id.hook_photo);
+
         mDisplayName = new DisplayNameEditor(context);
+        mDisplayName.swapWith(mContent, R.id.hook_displayname);
     }
 
     /**
@@ -88,6 +91,9 @@
         mGeneral.removeAllViews();
         mSecondary.removeAllViews();
 
+        // Bail if invalid state or source
+        if (state == null || source == null) return;
+
         // Create editor sections for each possible data kind
         for (DataKind kind : source.getSortedDataKinds()) {
             // Skip kind of not editable
@@ -304,7 +310,9 @@
             for (EditField field : kind.fieldList) {
                 // Inflate field from definition
                 EditText fieldView = (EditText)mInflater.inflate(RES_FIELD, mFields, false);
-                fieldView.setHint(field.titleRes);
+                if (field.titleRes != -1) {
+                    fieldView.setHint(field.titleRes);
+                }
                 fieldView.setInputType(field.inputType);
                 fieldView.setMinLines(field.minLines);
 
diff --git a/src/com/android/contacts/ui/widget/ViewHolder.java b/src/com/android/contacts/ui/widget/ViewHolder.java
index 2dec441..223dbb1 100644
--- a/src/com/android/contacts/ui/widget/ViewHolder.java
+++ b/src/com/android/contacts/ui/widget/ViewHolder.java
@@ -19,6 +19,9 @@
 import android.content.Context;
 import android.view.LayoutInflater;
 import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.widget.FrameLayout;
 
 /**
  * Helper to inflate a given layout and produce the {@link View} when requested.
@@ -34,6 +37,30 @@
         mContent = mInflater.inflate(layoutRes, null);
     }
 
+    public void swapWith(View target) {
+        // Borrow layout params and id for ourselves
+        this.mContent.setLayoutParams(target.getLayoutParams());
+        this.mContent.setId(target.getId());
+
+        // Find the direct parent of this view
+        final ViewParent parent = target.getParent();
+        if (parent == null || !(parent instanceof ViewGroup)) return;
+
+        // Swap out existing view with ourselves
+        final ViewGroup parentGroup = (ViewGroup)parent;
+        final int index = parentGroup.indexOfChild(target);
+        parentGroup.removeViewAt(index);
+        parentGroup.addView(this.mContent, index);
+    }
+
+    public void swapWith(View ancestor, int id) {
+        // Try finding the target view to replace
+        final View target = ancestor.findViewById(id);
+        if (target != null) {
+            swapWith(target);
+        }
+    }
+
     public View getView() {
         return mContent;
     }
diff --git a/tests/src/com/android/contacts/EntityModifierTests.java b/tests/src/com/android/contacts/EntityModifierTests.java
index 9d41f67..7b8580c 100644
--- a/tests/src/com/android/contacts/EntityModifierTests.java
+++ b/tests/src/com/android/contacts/EntityModifierTests.java
@@ -171,7 +171,8 @@
     }
 
     /**
-     * Test {@link EntityModifier#getBestValidType(EntityDelta, DataKind)}
+     * Test
+     * {@link EntityModifier#getBestValidType(EntityDelta, DataKind, boolean, int)}
      * by asserting expected best options in various states.
      */
     public void testBestValidType() {
@@ -187,27 +188,27 @@
 
         // Default suggestion should be home
         final EntityDelta state = getEntity();
-        suggested = EntityModifier.getBestValidType(state, kindPhone, false);
+        suggested = EntityModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
         assertEquals("Unexpected suggestion", typeHome, suggested);
 
         // Add first home, should now suggest work
         EntityModifier.insertChild(state, kindPhone, typeHome);
-        suggested = EntityModifier.getBestValidType(state, kindPhone, false);
+        suggested = EntityModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
         assertEquals("Unexpected suggestion", typeWork, suggested);
 
         // Add work fax, should still suggest work
         EntityModifier.insertChild(state, kindPhone, typeFaxWork);
-        suggested = EntityModifier.getBestValidType(state, kindPhone, false);
+        suggested = EntityModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
         assertEquals("Unexpected suggestion", typeWork, suggested);
 
         // Add other, should still suggest work
         EntityModifier.insertChild(state, kindPhone, typeOther);
-        suggested = EntityModifier.getBestValidType(state, kindPhone, false);
+        suggested = EntityModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
         assertEquals("Unexpected suggestion", typeWork, suggested);
 
         // Add work, now should suggest other
         EntityModifier.insertChild(state, kindPhone, typeWork);
-        suggested = EntityModifier.getBestValidType(state, kindPhone, false);
+        suggested = EntityModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
         assertEquals("Unexpected suggestion", typeOther, suggested);
     }
 }