Expandable structured support, organize editors, much more.

Wrote expandable editor support, so that StructuredName and
StructuredPostal could be displayed compactly on smaller
screens, but still expanded to edit all possible fields.

Reorganized editors to directly inflate classes instead of
using ViewHolder pattern.  This helps us prepare for focus
saving logic coming soon, and also required that each data
row have getViewId() to be uniquely identified.

When editing EAS contacts, don't use separate types for
"Email 1", "Email 2", etc, and instead use a single overall
limit on type-less values.  (This is dependant on
http://b/2065904 being resolved.)

Defined fallback "on-phone" account separately to prevent
confusion with Google account, also limited the possible
data types that could be entered.

Suspend aggregation while persisting RawContacts updates
to prevent possible re-aggregation and user confusion when
returning to View activity.  Also correctly generate
AggregationExceptions when inserting new RawContacts under
an existing aggregate.  Fixes http://b/2087517

When inserting Google contacts, add GroupMembership for
"My Contacts" to help visibility, fixing http://b/2070479

Don't reprompt for account when rotating during INSERT,
fixing http://b/2084585 and hide editor until finished
with async loading.  Ensure that specific rows exist so
we always have StructuredName to edit, and have waiting
Phone and Email fields for INSERT.

Used better method of creating generic ArrayList/HashMap.
diff --git a/src/com/android/contacts/ScrollingTabWidget.java b/src/com/android/contacts/ScrollingTabWidget.java
index 466071a..5714fa8 100644
--- a/src/com/android/contacts/ScrollingTabWidget.java
+++ b/src/com/android/contacts/ScrollingTabWidget.java
@@ -20,6 +20,7 @@
 import android.graphics.Canvas;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
+import android.os.Bundle;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.KeyEvent;
diff --git a/src/com/android/contacts/StyleManager.java b/src/com/android/contacts/StyleManager.java
index d2f6e09..2d24551 100644
--- a/src/com/android/contacts/StyleManager.java
+++ b/src/com/android/contacts/StyleManager.java
@@ -16,6 +16,8 @@
 
 package com.android.contacts;
 
+import com.android.contacts.model.Sources;
+
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -39,6 +41,10 @@
 import android.util.Xml;
 
 
+/**
+ * @deprecated Use {@link Sources} instead.
+ */
+@Deprecated
 public final class StyleManager extends BroadcastReceiver {
 
     public static final String TAG = "StyleManager";
diff --git a/src/com/android/contacts/model/ContactsSource.java b/src/com/android/contacts/model/ContactsSource.java
index a74309c..1ceed41 100644
--- a/src/com/android/contacts/model/ContactsSource.java
+++ b/src/com/android/contacts/model/ContactsSource.java
@@ -16,6 +16,8 @@
 
 package com.android.contacts.model;
 
+import com.google.android.collect.Lists;
+
 import org.xmlpull.v1.XmlPullParser;
 
 import android.accounts.Account;
@@ -89,7 +91,7 @@
      * {@link Account} or for matching against {@link Data#RES_PACKAGE}.
      */
     public String resPackageName;
-    
+
     public int titleRes;
     public int iconRes;
 
@@ -98,7 +100,7 @@
     /**
      * Set of {@link DataKind} supported by this source.
      */
-    private ArrayList<DataKind> mKinds = new ArrayList<DataKind>();
+    private ArrayList<DataKind> mKinds = Lists.newArrayList();
 
     private static final String ACTION_SYNC_ADAPTER = "android.content.SyncAdapter";
     private static final String METADATA_CONTACTS = "android.provider.CONTACTS_STRUCTURE";
@@ -126,7 +128,10 @@
 
         // Handle some well-known sources with hard-coded constraints
         // TODO: move these into adapter-specific XML once schema finalized
-        if (HardCodedSources.ACCOUNT_TYPE_GOOGLE.equals(accountType)) {
+        if (HardCodedSources.ACCOUNT_TYPE_FALLBACK.equals(accountType)) {
+            HardCodedSources.buildFallback(context, this);
+            return;
+        } else if (HardCodedSources.ACCOUNT_TYPE_GOOGLE.equals(accountType)) {
             HardCodedSources.buildGoogle(context, this);
             return;
         } else if(HardCodedSources.ACCOUNT_TYPE_EXCHANGE.equals(accountType)) {
diff --git a/src/com/android/contacts/model/Editor.java b/src/com/android/contacts/model/Editor.java
new file mode 100644
index 0000000..64c0952
--- /dev/null
+++ b/src/com/android/contacts/model/Editor.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.model;
+
+import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.model.EntityDelta.ValuesDelta;
+
+import android.provider.ContactsContract.Data;
+
+/**
+ * Generic definition of something that edits a {@link Data} row through an
+ * {@link ValuesDelta} object.
+ */
+public interface Editor {
+    /**
+     * Listener for an {@link Editor}, usually to handle deleted items.
+     */
+    public interface EditorListener {
+        /**
+         * Called when the given {@link Editor} has been deleted.
+         */
+        public void onDeleted(Editor editor);
+    }
+
+    /**
+     * Prepare this editor for the given {@link ValuesDelta}, which
+     * builds any needed views. Any changes performed by the user will be
+     * written back to that same object.
+     */
+    public void setValues(DataKind kind, ValuesDelta values, EntityDelta state);
+
+    /**
+     * Add a specific {@link EditorListener} to this {@link Editor}.
+     */
+    public void setEditorListener(EditorListener listener);
+
+    /**
+     * Called internally when the contents of a specific field have changed,
+     * allowing advanced editors to persist data in a specific way.
+     */
+    public void onFieldChanged(String column, String value);
+}
diff --git a/src/com/android/contacts/model/EntityDelta.java b/src/com/android/contacts/model/EntityDelta.java
index 427cf33..1b99881 100644
--- a/src/com/android/contacts/model/EntityDelta.java
+++ b/src/com/android/contacts/model/EntityDelta.java
@@ -16,6 +16,10 @@
 
 package com.android.contacts.model;
 
+import com.google.android.collect.Lists;
+import com.google.android.collect.Maps;
+import com.google.android.collect.Sets;
+
 import android.content.ContentProviderOperation;
 import android.content.ContentValues;
 import android.content.Entity;
@@ -25,8 +29,10 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.provider.BaseColumns;
+import android.provider.ContactsContract.AggregationExceptions;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.RawContacts;
+import android.view.View;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -59,14 +65,12 @@
      * Internal map of children values from {@link Entity#getSubValues()}, which
      * we store here sorted into {@link Data#MIMETYPE} bins.
      */
-    private HashMap<String, ArrayList<ValuesDelta>> mEntries;
+    private HashMap<String, ArrayList<ValuesDelta>> mEntries = Maps.newHashMap();
 
-    private EntityDelta() {
-        mEntries = new HashMap<String, ArrayList<ValuesDelta>>();
+    public EntityDelta() {
     }
 
     public EntityDelta(ValuesDelta values) {
-        this();
         mValues = values;
     }
 
@@ -143,7 +147,7 @@
     private ArrayList<ValuesDelta> getMimeEntries(String mimeType, boolean lazyCreate) {
         ArrayList<ValuesDelta> mimeEntries = mEntries.get(mimeType);
         if (mimeEntries == null && lazyCreate) {
-            mimeEntries = new ArrayList<ValuesDelta>();
+            mimeEntries = Lists.newArrayList();
             mEntries.put(mimeType, mimeEntries);
         }
         return mimeEntries;
@@ -153,6 +157,11 @@
         return getMimeEntries(mimeType, false);
     }
 
+    public int getMimeEntriesCount(String mimeType) {
+        final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false);
+        return mimeEntries == null ? 0 : mimeEntries.size();
+    }
+
     public boolean hasMimeEntries(String mimeType) {
         return mEntries.containsKey(mimeType);
     }
@@ -273,7 +282,7 @@
      * {@link EntityDelta} represents.
      */
     public ArrayList<ContentProviderOperation> buildDiff() {
-        final ArrayList<ContentProviderOperation> diff = new ArrayList<ContentProviderOperation>();
+        final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
 
         final boolean isContactInsert = mValues.isInsert();
         final boolean isContactDelete = mValues.isDelete();
@@ -281,8 +290,16 @@
         final Long beforeId = mValues.getId();
         final Long beforeVersion = mValues.getAsLong(RawContacts.VERSION);
 
+        Builder builder;
+
+        // Suspend aggregation while persisting edits
+        if (mValues.beforeExists()) {
+            builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_SUSPENDED);
+            possibleAdd(diff, builder);
+        }
+
         // Build possible operation at Contact level
-        Builder builder = mValues.buildDiff(RawContacts.CONTENT_URI);
+        builder = mValues.buildDiff(RawContacts.CONTENT_URI);
         possibleAdd(diff, builder);
 
         // Build operations for all children
@@ -308,18 +325,46 @@
             }
         }
 
+        // Create exception when insert requested aggregate membership
+        final Long contactId = mValues.getAsLong(RawContacts.CONTACT_ID);
+        if (isContactInsert && contactId != null) {
+            builder = ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
+            builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_IN);
+            builder.withValue(AggregationExceptions.CONTACT_ID, contactId);
+            builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID, 0);
+            possibleAdd(diff, builder);
+        }
+
+        // Enable aggregation when finished with updates
+        if (mValues.beforeExists()) {
+            builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_DEFAULT);
+            possibleAdd(diff, builder);
+        }
+
         // If any operations, assert that version is identical so we bail if changed
         if (diff.size() > 0 && beforeVersion != null && beforeId != null) {
             builder = ContentProviderOperation.newAssertQuery(RawContacts.CONTENT_URI);
             builder.withSelection(RawContacts._ID + "=" + beforeId, null);
             builder.withValue(RawContacts.VERSION, beforeVersion);
-            // Sneak version check at beginning of list
+            // Sneak version check at beginning of list--we only depend on
+            // back-references during insert cases.
             diff.add(0, builder.build());
         }
 
         return diff;
     }
 
+    /**
+     * Build a {@link ContentProviderOperation} that changes
+     * {@link RawContacts#AGGREGATION_MODE} to the given value.
+     */
+    protected Builder buildSetAggregationMode(Long beforeId, int mode) {
+        Builder builder = ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI);
+        builder.withValue(RawContacts.AGGREGATION_MODE, mode);
+        builder.withSelection(RawContacts._ID + "=" + beforeId, null);
+        return builder;
+    }
+
     /** {@inheritDoc} */
     public int describeContents() {
         // Nothing special about this parcel
@@ -369,6 +414,14 @@
         private ContentValues mAfter;
         private String mIdColumn = BaseColumns._ID;
 
+        /**
+         * Next value to assign to {@link #mIdColumn} when building an insert
+         * operation through {@link #fromAfter(ContentValues)}. This is used so
+         * we can concretely reference this {@link ValuesDelta} before it has
+         * been persisted.
+         */
+        private static int sNextInsertId = -1;
+
         private ValuesDelta() {
         }
 
@@ -391,6 +444,9 @@
             final ValuesDelta entry = new ValuesDelta();
             entry.mBefore = null;
             entry.mAfter = after;
+
+            // Assign temporary id which is dropped before insert.
+            entry.mAfter.put(entry.mIdColumn, sNextInsertId--);
             return entry;
         }
 
@@ -432,6 +488,14 @@
             return getAsLong(mIdColumn);
         }
 
+        /**
+         * Return a valid integer value suitable for {@link View#setId(int)}.
+         */
+        public int getViewId() {
+            final Long id = this.getId();
+            return (id == null) ? View.NO_ID : id.intValue();
+        }
+
         public void setIdColumn(String idColumn) {
             mIdColumn = idColumn;
         }
@@ -492,7 +556,7 @@
          * Return set of all keys defined through this object.
          */
         public Set<String> keySet() {
-            final HashSet<String> keys = new HashSet<String>();
+            final HashSet<String> keys = Sets.newHashSet();
 
             if (mBefore != null) {
                 for (Map.Entry<String, Object> entry : mBefore.valueSet()) {
@@ -570,6 +634,7 @@
             Builder builder = null;
             if (isInsert()) {
                 // Changed values are "insert" back-referenced to Contact
+                mAfter.remove(mIdColumn);
                 builder = ContentProviderOperation.newInsert(targetUri);
                 builder.withValues(mAfter);
             } else if (isDelete()) {
diff --git a/src/com/android/contacts/model/EntityModifier.java b/src/com/android/contacts/model/EntityModifier.java
index 9da140e..1c3c8f7 100644
--- a/src/com/android/contacts/model/EntityModifier.java
+++ b/src/com/android/contacts/model/EntityModifier.java
@@ -19,6 +19,7 @@
 import com.android.contacts.model.ContactsSource.DataKind;
 import com.android.contacts.model.ContactsSource.EditType;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.google.android.collect.Lists;
 
 import android.content.ContentValues;
 import android.content.Context;
@@ -26,7 +27,6 @@
 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;
@@ -34,7 +34,6 @@
 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;
@@ -68,6 +67,20 @@
     }
 
     /**
+     * Ensure that at least one of the given {@link DataKind} exists in the
+     * given {@link EntityDelta} state, and try creating one if none exist.
+     */
+    public static void ensureKindExists(EntityDelta state, ContactsSource source, String mimeType) {
+        final DataKind kind = source.getKindForMimetype(mimeType);
+        final boolean hasChild = state.getMimeEntriesCount(mimeType) > 0;
+
+        if (!hasChild && kind != null) {
+            // Create child when none exists and valid kind
+            insertChild(state, kind);
+        }
+    }
+
+    /**
      * For the given {@link EntityDelta} and {@link DataKind}, return the
      * list possible {@link EditType} options available based on
      * {@link ContactsSource}.
@@ -106,7 +119,7 @@
      */
     private static ArrayList<EditType> getValidTypes(EntityDelta state, DataKind kind,
             EditType forceInclude, boolean includeSecondary, SparseIntArray typeCount) {
-        final ArrayList<EditType> validTypes = new ArrayList<EditType>();
+        final ArrayList<EditType> validTypes = Lists.newArrayList();
 
         // Bail early if no types provided
         if (!hasEditTypes(kind)) return validTypes;
@@ -297,7 +310,8 @@
      * Parse the given {@link Bundle} into the given {@link EntityDelta} state,
      * assuming the extras defined through {@link Intents}.
      */
-    public static void parseExtras(Context context, ContactsSource source, EntityDelta state, Bundle extras) {
+    public static void parseExtras(Context context, ContactsSource source, EntityDelta state,
+            Bundle extras) {
         if (extras == null || extras.size() == 0) {
             // Bail early if no useful data
             return;
diff --git a/src/com/android/contacts/model/HardCodedSources.java b/src/com/android/contacts/model/HardCodedSources.java
index fb00639..d9fd83c 100644
--- a/src/com/android/contacts/model/HardCodedSources.java
+++ b/src/com/android/contacts/model/HardCodedSources.java
@@ -21,11 +21,17 @@
 import com.android.contacts.model.ContactsSource.EditField;
 import com.android.contacts.model.ContactsSource.EditType;
 import com.android.contacts.model.ContactsSource.StringInflater;
+import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.google.android.collect.Lists;
 
+import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Groups;
 import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
 import android.provider.ContactsContract.CommonDataKinds.Im;
 import android.provider.ContactsContract.CommonDataKinds.Nickname;
 import android.provider.ContactsContract.CommonDataKinds.Note;
@@ -35,6 +41,7 @@
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.provider.ContactsContract.Contacts.Data;
 import android.view.inputmethod.EditorInfo;
 
 import java.util.ArrayList;
@@ -49,6 +56,7 @@
     public static final String ACCOUNT_TYPE_GOOGLE = "com.google.GAIA";
     public static final String ACCOUNT_TYPE_EXCHANGE = "com.android.exchange";
     public static final String ACCOUNT_TYPE_FACEBOOK = "com.facebook.auth.login";
+    public static final String ACCOUNT_TYPE_FALLBACK = "com.example.fallback-contacts";
 
     private static final int FLAGS_PHONE = EditorInfo.TYPE_CLASS_PHONE;
     private static final int FLAGS_EMAIL = EditorInfo.TYPE_CLASS_TEXT
@@ -72,6 +80,48 @@
     }
 
     /**
+     * Hard-coded instance of {@link ContactsSource} for fallback use.
+     */
+    static void buildFallback(Context context, ContactsSource list) {
+        {
+            // FALLBACK: STRUCTUREDNAME
+            DataKind kind = new DataKind(StructuredName.CONTENT_ITEM_TYPE,
+                    R.string.nameLabelsGroup, -1, -1, true);
+            list.add(kind);
+        }
+
+        {
+            // FALLBACK: PHONE
+            DataKind kind = new DataKind(Phone.CONTENT_ITEM_TYPE,
+                    R.string.phoneLabelsGroup, android.R.drawable.sym_action_call, 10, true);
+            kind.iconAltRes = R.drawable.sym_action_sms;
+
+            kind.actionHeader = new ActionInflater(list.resPackageName, kind);
+            kind.actionAltHeader = new ActionAltInflater(list.resPackageName, kind);
+            kind.actionBody = new SimpleInflater(Phone.NUMBER);
+
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+
+            list.add(kind);
+        }
+
+        {
+            // GOOGLE: EMAIL
+            DataKind kind = new DataKind(Email.CONTENT_ITEM_TYPE,
+                    R.string.emailLabelsGroup, android.R.drawable.sym_action_email, 15, true);
+
+            kind.actionHeader = new ActionInflater(list.resPackageName, kind);
+            kind.actionBody = new SimpleInflater(Email.DATA);
+
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+            list.add(kind);
+        }
+    }
+
+    /**
      * Hard-coded instance of {@link ContactsSource} for Google Contacts.
      */
     static void buildGoogle(Context context, ContactsSource list) {
@@ -79,6 +129,25 @@
             // GOOGLE: STRUCTUREDNAME
             DataKind kind = new DataKind(StructuredName.CONTENT_ITEM_TYPE,
                     R.string.nameLabelsGroup, -1, -1, true);
+
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
+                    FLAGS_PERSON_NAME, true));
+            kind.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
+                    FLAGS_PERSON_NAME));
+            kind.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
+                    FLAGS_PERSON_NAME, true));
+            kind.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
+                    FLAGS_PERSON_NAME));
+            kind.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
+                    FLAGS_PERSON_NAME, true));
+            kind.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME,
+                    R.string.name_phonetic_given, FLAGS_PHONETIC, true));
+            kind.fieldList.add(new EditField(StructuredName.PHONETIC_MIDDLE_NAME,
+                    R.string.name_phonetic_middle, FLAGS_PHONETIC, true));
+            kind.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME,
+                    R.string.name_phonetic_family, FLAGS_PHONETIC, true));
+
             list.add(kind);
         }
 
@@ -99,7 +168,7 @@
             kind.actionBody = new SimpleInflater(Phone.NUMBER);
 
             kind.typeColumn = Phone.TYPE;
-            kind.typeList = new ArrayList<EditType>();
+            kind.typeList = Lists.newArrayList();
             kind.typeList.add(new EditType(Phone.TYPE_HOME, R.string.type_home, R.string.call_home,
                     R.string.sms_home));
             kind.typeList.add(new EditType(Phone.TYPE_MOBILE, R.string.type_mobile,
@@ -118,7 +187,7 @@
                     R.string.call_custom, R.string.sms_custom).setSecondary(true).setCustomColumn(
                     Phone.LABEL));
 
-            kind.fieldList = new ArrayList<EditField>();
+            kind.fieldList = Lists.newArrayList();
             kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
 
             list.add(kind);
@@ -133,7 +202,7 @@
             kind.actionBody = new SimpleInflater(Email.DATA);
 
             kind.typeColumn = Email.TYPE;
-            kind.typeList = new ArrayList<EditType>();
+            kind.typeList = Lists.newArrayList();
             kind.typeList
                     .add(new EditType(Email.TYPE_HOME, R.string.type_home, R.string.email_home));
             kind.typeList
@@ -143,7 +212,7 @@
             kind.typeList.add(new EditType(Email.TYPE_CUSTOM, R.string.type_custom,
                     R.string.email_home).setSecondary(true).setCustomColumn(Email.LABEL));
 
-            kind.fieldList = new ArrayList<EditField>();
+            kind.fieldList = Lists.newArrayList();
             kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
 
             list.add(kind);
@@ -164,7 +233,7 @@
             kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);
 
             kind.typeColumn = Im.PROTOCOL;
-            kind.typeList = new ArrayList<EditType>();
+            kind.typeList = Lists.newArrayList();
             kind.typeList.add(new EditType(Im.PROTOCOL_AIM, R.string.type_im_aim));
             kind.typeList.add(new EditType(Im.PROTOCOL_MSN, R.string.type_im_msn));
             kind.typeList.add(new EditType(Im.PROTOCOL_YAHOO, R.string.type_im_yahoo));
@@ -176,7 +245,7 @@
             kind.typeList.add(new EditType(Im.PROTOCOL_CUSTOM, R.string.type_custom).setSecondary(
                     true).setCustomColumn(Im.CUSTOM_PROTOCOL));
 
-            kind.fieldList = new ArrayList<EditField>();
+            kind.fieldList = Lists.newArrayList();
             kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
 
             list.add(kind);
@@ -192,7 +261,7 @@
             kind.actionBody = new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS);
 
             kind.typeColumn = StructuredPostal.TYPE;
-            kind.typeList = new ArrayList<EditType>();
+            kind.typeList = Lists.newArrayList();
             kind.typeList.add(new EditType(StructuredPostal.TYPE_HOME, R.string.type_home,
                     R.string.map_home));
             kind.typeList.add(new EditType(StructuredPostal.TYPE_WORK, R.string.type_work,
@@ -204,7 +273,7 @@
                             R.string.map_custom).setSecondary(true).setCustomColumn(
                             StructuredPostal.LABEL));
 
-            kind.fieldList = new ArrayList<EditField>();
+            kind.fieldList = Lists.newArrayList();
             kind.fieldList.add(new EditField(StructuredPostal.STREET, R.string.postal_street,
                     FLAGS_POSTAL));
             kind.fieldList.add(new EditField(StructuredPostal.POBOX, R.string.postal_pobox,
@@ -233,13 +302,13 @@
             kind.actionBody = new SimpleInflater(Organization.TITLE);
 
             kind.typeColumn = Organization.TYPE;
-            kind.typeList = new ArrayList<EditType>();
+            kind.typeList = Lists.newArrayList();
             kind.typeList.add(new EditType(Organization.TYPE_WORK, R.string.type_work));
             kind.typeList.add(new EditType(Organization.TYPE_OTHER, R.string.type_other));
             kind.typeList.add(new EditType(Organization.TYPE_CUSTOM, R.string.type_custom)
                     .setSecondary(true).setCustomColumn(Organization.LABEL));
 
-            kind.fieldList = new ArrayList<EditField>();
+            kind.fieldList = Lists.newArrayList();
             kind.fieldList.add(new EditField(Organization.COMPANY, R.string.ghostData_company,
                     FLAGS_GENERIC_NAME));
             kind.fieldList.add(new EditField(Organization.TITLE, R.string.ghostData_title,
@@ -257,7 +326,7 @@
             kind.actionHeader = new SimpleInflater(list.resPackageName, R.string.label_notes);
             kind.actionBody = new SimpleInflater(Note.NOTE);
 
-            kind.fieldList = new ArrayList<EditField>();
+            kind.fieldList = Lists.newArrayList();
             kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));
 
             list.add(kind);
@@ -272,7 +341,7 @@
             kind.actionHeader = new SimpleInflater(list.resPackageName, R.string.nicknameLabelsGroup);
             kind.actionBody = new SimpleInflater(Nickname.NAME);
 
-            kind.fieldList = new ArrayList<EditField>();
+            kind.fieldList = Lists.newArrayList();
             kind.fieldList.add(new EditField(Nickname.NAME, R.string.nicknameLabelsGroup,
                     FLAGS_PERSON_NAME));
 
@@ -280,23 +349,46 @@
         }
 
         // TODO: GOOGLE: GROUPMEMBERSHIP
-        // TODO: GOOGLE: WEBSITE
+
+        {
+            // GOOGLE: WEBSITE
+            DataKind kind = new DataKind(Website.CONTENT_ITEM_TYPE,
+                    R.string.websiteLabelsGroup, -1, 120, true);
+            kind.secondary = true;
+
+            kind.actionHeader = new SimpleInflater(list.resPackageName, R.string.websiteLabelsGroup);
+            kind.actionBody = new SimpleInflater(Website.URL);
+
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE));
+
+            list.add(kind);
+        }
+    }
+
+    // TODO: this should come from resource in the future
+    private static final String GOOGLE_MY_CONTACTS_GROUP = "System Group: My Contacts";
+
+    public static final ValuesDelta buildMyContactsMembership(Context context) {
+        final ContentResolver resolver = context.getContentResolver();
+        final Cursor cursor = resolver.query(Groups.CONTENT_URI, new String[] { Groups.SOURCE_ID },
+                Groups.TITLE + "=?", new String[] { GOOGLE_MY_CONTACTS_GROUP }, null);
+
+        final ContentValues values = new ContentValues();
+        if (cursor.moveToFirst()) {
+            final String sourceId = cursor.getString(0);
+            values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
+            values.put(GroupMembership.GROUP_SOURCE_ID, sourceId);
+        }
+
+        cursor.close();
+        return ValuesDelta.fromAfter(values);
     }
 
     /**
      * The constants below are shared with the Exchange sync adapter, and are
      * currently static. These values should be maintained in parallel.
      */
-    private static final int TYPE_EMAIL1 = 20;
-    private static final int TYPE_EMAIL2 = 21;
-    private static final int TYPE_EMAIL3 = 22;
-
-    private static final int TYPE_IM1 = 23;
-    private static final int TYPE_IM2 = 24;
-    private static final int TYPE_IM3 = 25;
-
-    private static final int TYPE_WORK2 = 26;
-    private static final int TYPE_HOME2 = 27;
     private static final int TYPE_CAR = 28;
     private static final int TYPE_COMPANY_MAIN = 29;
     private static final int TYPE_MMS = 30;
@@ -311,6 +403,23 @@
             DataKind kind = new DataKind(StructuredName.CONTENT_ITEM_TYPE,
                     R.string.nameLabelsGroup, -1, -1, true);
             kind.typeOverallMax = 1;
+
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
+                    FLAGS_PERSON_NAME, true));
+            kind.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
+                    FLAGS_PERSON_NAME));
+            kind.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
+                    FLAGS_PERSON_NAME, true));
+            kind.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
+                    FLAGS_PERSON_NAME));
+            kind.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
+                    FLAGS_PERSON_NAME, true));
+            kind.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME,
+                    R.string.name_phonetic_given, FLAGS_PHONETIC, true));
+            kind.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME,
+                    R.string.name_phonetic_family, FLAGS_PHONETIC, true));
+
             list.add(kind);
         }
 
@@ -332,17 +441,13 @@
             kind.actionBody = new SimpleInflater(Phone.NUMBER);
 
             kind.typeColumn = Phone.TYPE;
-            kind.typeList = new ArrayList<EditType>();
+            kind.typeList = Lists.newArrayList();
             kind.typeList.add(new EditType(Phone.TYPE_HOME, R.string.type_home, R.string.call_home,
-                    R.string.sms_home).setSpecificMax(1));
-            kind.typeList.add(new EditType(TYPE_HOME2, R.string.type_home_2, R.string.call_home_2,
-                    R.string.sms_home_2).setSecondary(true).setSpecificMax(1));
+                    R.string.sms_home).setSpecificMax(2));
             kind.typeList.add(new EditType(Phone.TYPE_MOBILE, R.string.type_mobile,
                     R.string.call_mobile, R.string.sms_mobile).setSpecificMax(1));
             kind.typeList.add(new EditType(Phone.TYPE_WORK, R.string.type_work, R.string.call_work,
-                    R.string.sms_work).setSpecificMax(1));
-            kind.typeList.add(new EditType(TYPE_WORK2, R.string.type_work_2, R.string.call_work_2,
-                    R.string.sms_work_2).setSecondary(true).setSpecificMax(1));
+                    R.string.sms_work).setSpecificMax(2));
             kind.typeList.add(new EditType(Phone.TYPE_FAX_WORK, R.string.type_fax_work,
                     R.string.call_fax_work, R.string.sms_fax_work).setSecondary(true)
                     .setSpecificMax(1));
@@ -364,7 +469,7 @@
                     R.string.call_custom, R.string.sms_custom).setSecondary(true).setSpecificMax(1)
                     .setCustomColumn(Phone.LABEL));
 
-            kind.fieldList = new ArrayList<EditField>();
+            kind.fieldList = Lists.newArrayList();
             kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
 
             list.add(kind);
@@ -377,17 +482,9 @@
 
             kind.actionHeader = new ActionInflater(list.resPackageName, kind);
             kind.actionBody = new SimpleInflater(Email.DATA);
+            kind.typeOverallMax = 3;
 
-            kind.typeColumn = Email.TYPE;
-            kind.typeList = new ArrayList<EditType>();
-            kind.typeList.add(new EditType(TYPE_EMAIL1, R.string.type_email_1, R.string.email_1)
-                    .setSpecificMax(1));
-            kind.typeList.add(new EditType(TYPE_EMAIL2, R.string.type_email_2, R.string.email_2)
-                    .setSpecificMax(1));
-            kind.typeList.add(new EditType(TYPE_EMAIL3, R.string.type_email_3, R.string.email_3)
-                    .setSpecificMax(1));
-
-            kind.fieldList = new ArrayList<EditField>();
+            kind.fieldList = Lists.newArrayList();
             kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
 
             list.add(kind);
@@ -400,14 +497,9 @@
 
             kind.actionHeader = new ActionInflater(list.resPackageName, kind);
             kind.actionBody = new SimpleInflater(Im.DATA);
+            kind.typeOverallMax = 3;
 
-            kind.typeColumn = Im.TYPE;
-            kind.typeList = new ArrayList<EditType>();
-            kind.typeList.add(new EditType(TYPE_IM1, R.string.type_im_1).setSpecificMax(1));
-            kind.typeList.add(new EditType(TYPE_IM2, R.string.type_im_2).setSpecificMax(1));
-            kind.typeList.add(new EditType(TYPE_IM3, R.string.type_im_3).setSpecificMax(1));
-
-            kind.fieldList = new ArrayList<EditField>();
+            kind.fieldList = Lists.newArrayList();
             kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
 
             list.add(kind);
@@ -423,7 +515,7 @@
             kind.actionHeader = new SimpleInflater(list.resPackageName, R.string.nicknameLabelsGroup);
             kind.actionBody = new SimpleInflater(Nickname.NAME);
 
-            kind.fieldList = new ArrayList<EditField>();
+            kind.fieldList = Lists.newArrayList();
             kind.fieldList.add(new EditField(Nickname.NAME, R.string.nicknameLabelsGroup,
                     FLAGS_PERSON_NAME));
 
@@ -440,7 +532,7 @@
             kind.actionHeader = new SimpleInflater(list.resPackageName, R.string.websiteLabelsGroup);
             kind.actionBody = new SimpleInflater(Website.URL);
 
-            kind.fieldList = new ArrayList<EditField>();
+            kind.fieldList = Lists.newArrayList();
             kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE));
 
             list.add(kind);
diff --git a/src/com/android/contacts/model/Sources.java b/src/com/android/contacts/model/Sources.java
index e19d1b6..c0f49c6 100644
--- a/src/com/android/contacts/model/Sources.java
+++ b/src/com/android/contacts/model/Sources.java
@@ -16,6 +16,10 @@
 
 package com.android.contacts.model;
 
+import com.android.contacts.R;
+import com.google.android.collect.Lists;
+import com.google.android.collect.Maps;
+
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.accounts.AuthenticatorDescription;
@@ -39,11 +43,11 @@
 public class Sources {
     private static final String TAG = "Sources";
 
-    public static final String ACCOUNT_TYPE_FALLBACK = HardCodedSources.ACCOUNT_TYPE_GOOGLE;
+    public static final String ACCOUNT_TYPE_FALLBACK = HardCodedSources.ACCOUNT_TYPE_FALLBACK;
 
     private Context mContext;
 
-    private HashMap<String, ContactsSource> mSources = new HashMap<String, ContactsSource>();
+    private HashMap<String, ContactsSource> mSources = Maps.newHashMap();
 
     private static SoftReference<Sources> sInstance = null;
 
@@ -77,6 +81,17 @@
     protected void loadAccounts() {
         mSources.clear();
 
+        {
+            // Create fallback contacts source for on-phone contacts
+            final ContactsSource source = new ContactsSource();
+            source.accountType = HardCodedSources.ACCOUNT_TYPE_FALLBACK;
+            source.resPackageName = mContext.getPackageName();
+            source.titleRes = R.string.account_phone;
+            source.iconRes = R.drawable.ic_launcher_contacts;
+
+            mSources.put(source.accountType, source);
+        }
+
         final AccountManager am = AccountManager.get(mContext);
         final IContentService cs = ContentResolver.getContentService();
 
@@ -122,7 +137,7 @@
         }
         throw new IllegalStateException("Couldn't find authenticator for specific account type");
     }
-    
+
     /**
      * Return list of all known, writable {@link ContactsSource}. Sources
      * returned may require inflation before they can be used.
@@ -130,7 +145,7 @@
     public ArrayList<Account> getAccounts(boolean writableOnly) {
         final AccountManager am = AccountManager.get(mContext);
         final Account[] accounts = am.getAccounts();
-        final ArrayList<Account> matching = new ArrayList<Account>();
+        final ArrayList<Account> matching = Lists.newArrayList();
 
         for (Account account : accounts) {
             // Ensure we have details loaded for each account
diff --git a/src/com/android/contacts/ui/DisplayGroupsActivity.java b/src/com/android/contacts/ui/DisplayGroupsActivity.java
index dbc3be2..dffbfda 100644
--- a/src/com/android/contacts/ui/DisplayGroupsActivity.java
+++ b/src/com/android/contacts/ui/DisplayGroupsActivity.java
@@ -20,6 +20,7 @@
 import com.android.contacts.model.ContactsSource;
 import com.android.contacts.model.Sources;
 import com.android.contacts.util.WeakAsyncTask;
+import com.google.android.collect.Sets;
 
 import android.accounts.Account;
 import android.app.Activity;
@@ -171,7 +172,7 @@
             target.startManagingCursor(cursor);
 
             // Make records for each account known by Settings
-            final HashSet<Account> knownAccounts = new HashSet<Account>();
+            final HashSet<Account> knownAccounts = Sets.newHashSet();
             while (cursor.moveToNext()) {
                 final String accountName = cursor.getString(SettingsQuery.ACCOUNT_NAME);
                 final String accountType = cursor.getString(SettingsQuery.ACCOUNT_TYPE);
diff --git a/src/com/android/contacts/ui/EditContactActivity.java b/src/com/android/contacts/ui/EditContactActivity.java
index d96a958..d6acf30 100644
--- a/src/com/android/contacts/ui/EditContactActivity.java
+++ b/src/com/android/contacts/ui/EditContactActivity.java
@@ -24,12 +24,14 @@
 import com.android.contacts.model.ContactsSource;
 import com.android.contacts.model.EntityDelta;
 import com.android.contacts.model.EntityModifier;
+import com.android.contacts.model.HardCodedSources;
 import com.android.contacts.model.Sources;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
 import com.android.contacts.ui.widget.ContactEditorView;
 import com.android.contacts.util.EmptyService;
 import com.android.contacts.util.WeakAsyncTask;
 import com.android.internal.widget.ContactHeaderWidget;
+import com.google.android.collect.Lists;
 
 import android.accounts.Account;
 import android.app.Activity;
@@ -52,7 +54,11 @@
 import android.os.Parcelable;
 import android.os.RemoteException;
 import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.Contacts.Data;
 import android.util.Log;
@@ -87,24 +93,24 @@
     private static final int TOKEN_ENTITY = 41;
 
     private static final String KEY_EDIT_STATE = "state";
-    private static final String KEY_EDITOR_STATE = "editor";
     private static final String KEY_SELECTED_TAB = "tab";
-    private static final String KEY_SELECTED_TAB_ID = "tabId";
-    private static final String KEY_CONTACT_ID = "contactId";
+//    private static final String KEY_SELECTED_TAB_ID = "tabId";
+//    private static final String KEY_CONTACT_ID = "contactId";
 
-    private long mSelectedRawContactId = -1;
-    private long mContactId = -1;
+//    private int mSelectedTab = -1;
+
+//    private long mSelectedRawContactId = -1;
+//    private long mContactId = -1;
 
     private ScrollingTabWidget mTabWidget;
     private ContactHeaderWidget mHeader;
 
-    private View mTabContent;
     private ContactEditorView mEditor;
 
     private EditState mState = new EditState();
 
     private static class EditState extends ArrayList<EntityDelta> implements Parcelable {
-        public long getAggregateId() {
+        public long getContactId() {
             if (this.size() > 0) {
                 // Assume the aggregate tied to first child
                 final EntityDelta first = this.get(0);
@@ -115,6 +121,15 @@
             }
         }
 
+        public long getRawContactId(int index) {
+            if (index >=0 && index < this.size()) {
+                final EntityDelta delta = this.get(index);
+                return delta.getValues().getAsLong(RawContacts._ID);
+            } else {
+                return -1;
+            }
+        }
+
         /** {@inheritDoc} */
         public int describeContents() {
             // Nothing special about this parcel
@@ -171,10 +186,7 @@
         mTabWidget = (ScrollingTabWidget)this.findViewById(R.id.tab_widget);
         mTabWidget.setTabSelectionListener(this);
 
-        mTabContent = this.findViewById(android.R.id.tabcontent);
-
-        mEditor = new ContactEditorView(context);
-        mEditor.swapWith(mTabContent);
+        mEditor = (ContactEditorView)this.findViewById(android.R.id.tabcontent);
 
         findViewById(R.id.btn_done).setOnClickListener(this);
         findViewById(R.id.btn_discard).setOnClickListener(this);
@@ -183,7 +195,7 @@
             // Read initial state from database
             new QueryEntitiesTask(this).execute(intent);
 
-        } else if (Intent.ACTION_INSERT.equals(action)) {
+        } else if (Intent.ACTION_INSERT.equals(action) && icicle == null) {
             // Trigger dialog to pick account type
             doAddAction();
 
@@ -206,18 +218,23 @@
             final ContentResolver resolver = context.getContentResolver();
 
             // Handle both legacy and new authorities
-            String selection = "0";
             final Uri data = intent.getData();
             final String authority = data.getAuthority();
+            final String mimeType = intent.resolveType(resolver);
+
+            String selection = "0";
             if (ContactsContract.AUTHORITY.equals(authority)) {
-                final long rawContactId = ContentUris.parseId(data);
-                target.mSelectedRawContactId = rawContactId;
-                target.mContactId = ContactsUtils.queryForContactId(target.getContentResolver(),
-                        rawContactId);
-                selection = RawContacts.CONTACT_ID + "=" + target.mContactId;
+                if (Contacts.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                    // Handle selected aggregate
+                    final long contactId = ContentUris.parseId(data);
+                    selection = RawContacts.CONTACT_ID + "=" + contactId;
+                } else if (RawContacts.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                    final long rawContactId = ContentUris.parseId(data);
+                    final long contactId = ContactsUtils.queryForContactId(resolver, rawContactId);
+                    selection = RawContacts.CONTACT_ID + "=" + contactId;
+                }
             } else if (android.provider.Contacts.AUTHORITY.equals(authority)) {
                 final long rawContactId = ContentUris.parseId(data);
-                target.mSelectedRawContactId = rawContactId;
                 selection = RawContacts._ID + "=" + rawContactId;
             }
 
@@ -275,10 +292,9 @@
     protected void onSaveInstanceState(Bundle outState) {
         // Store entities with modifications
         outState.putParcelable(KEY_EDIT_STATE, mState);
-//        outState.putSparseParcelableArray(KEY_EDITOR_STATE, buildEditorState());
-//        outState.putInt(KEY_SELECTED_TAB, mTabWidget.getCurrentTab());
-        outState.putLong(KEY_SELECTED_TAB_ID, mSelectedRawContactId);
-        outState.putLong(KEY_CONTACT_ID, mContactId);
+        outState.putInt(KEY_SELECTED_TAB, mTabWidget.getCurrentTab());
+//        outState.putLong(KEY_SELECTED_TAB_ID, mSelectedRawContactId);
+//        outState.putLong(KEY_CONTACT_ID, mContactId);
 
         super.onSaveInstanceState(outState);
     }
@@ -287,17 +303,18 @@
     protected void onRestoreInstanceState(Bundle savedInstanceState) {
         // Read modifications from instance
         mState = savedInstanceState.<EditState> getParcelable(KEY_EDIT_STATE);
-        mSelectedRawContactId = savedInstanceState.getLong(KEY_SELECTED_TAB_ID);
-        mContactId = savedInstanceState.getLong(KEY_CONTACT_ID);
+
+//        mSelectedRawContactId = savedInstanceState.getLong(KEY_SELECTED_TAB_ID);
+//        mContactId = savedInstanceState.getLong(KEY_CONTACT_ID);
 
         Log.d(TAG, "onrestoreinstancestate");
 
-//        mEditorState = savedInstanceState.getSparseParcelableArray(KEY_EDITOR_STATE);
-//
-//        final int selectedTab = savedInstanceState.getInt(KEY_SELECTED_TAB);
         bindTabs();
         bindHeader();
 
+        final int selectedTab = savedInstanceState.getInt(KEY_SELECTED_TAB);
+        mTabWidget.setCurrentTab(selectedTab);
+
         // Restore selected tab and any focus
         super.onRestoreInstanceState(savedInstanceState);
     }
@@ -320,22 +337,22 @@
             final ContactsSource source = sources.getInflatedSource(accountType,
                     ContactsSource.LEVEL_CONSTRAINTS);
 
-            if (rawContactId != null && rawContactId == mSelectedRawContactId) {
-                selectedTab = mTabWidget.getTabCount();
-            }
-
             final View tabView = BaseContactCardActivity.createTabIndicatorView(
                     mTabWidget.getTabParent(), source);
             mTabWidget.addTab(tabView);
         }
+
         if (mState.size() > 0) {
             mTabWidget.setCurrentTab(selectedTab);
             this.onTabSelectionChanged(selectedTab, false);
         }
+
+        // Show editor now that we've loaded state
+        mEditor.setVisibility(View.VISIBLE);
     }
 
     /**
-     * Bind our header based on {@link #mEntities}, which include any edits.
+     * Bind our header based on {@link #mState}, which include any edits.
      * Usually called once {@link Entity} data has been loaded, or after a
      * primary {@link Data} change.
      */
@@ -345,9 +362,9 @@
         // TODO: fill header bar with newly parsed data for speed
         // TODO: handle legacy case correctly instead of assuming _id
 
-        if (mContactId > 0) {
-            mHeader.bindFromContactId(mContactId);
-        }
+//        if (mContactId > 0) {
+//            mHeader.bindFromContactId(mContactId);
+//        }
 
 //        mHeader.setDisplayName(displayName, phoneticName);
 //        mHeader.setPhoto(bitmap);
@@ -363,10 +380,6 @@
         // Find entity and source for selected tab
         final EntityDelta entity = mState.get(tabIndex);
         final String accountType = entity.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
-        Long rawContactId = entity.getValues().getAsLong(RawContacts._ID);
-        if (rawContactId != null) {
-            mSelectedRawContactId = rawContactId;
-        }
 
         final Sources sources = Sources.getInstance(this);
         final ContactsSource source = sources.getInflatedSource(accountType,
@@ -450,6 +463,9 @@
         // TODO: show or hide photo items based on current tab
         // hide photo stuff entirely if on read-only source
 
+        menu.findItem(R.id.menu_photo_add).setVisible(false);
+        menu.findItem(R.id.menu_photo_remove).setVisible(false);
+
         return true;
     }
 
@@ -559,6 +575,15 @@
      * finishes the activity.
      */
     private boolean doSaveAction() {
+        // Pass back last-selected contact
+        final int selectedTab = mTabWidget.getCurrentTab();
+        final long rawContactId = mState.getRawContactId(selectedTab);
+        if (rawContactId != -1) {
+            final Intent intent = new Intent();
+            intent.putExtra(ViewContactActivity.RAW_CONTACT_ID_EXTRA, rawContactId);
+            setResult(RESULT_OK, intent);
+        }
+
         try {
             final PersistTask task = new PersistTask(this);
             task.execute(mState);
@@ -573,8 +598,6 @@
 
         // Persisting finished, or we timed out waiting on it. Either way,
         // finish this activity, the background task will keep running.
-        setResult(RESULT_OK, new Intent().putExtra(ViewContactActivity.RAW_CONTACT_ID_EXTRA,
-                mSelectedRawContactId));
         this.finish();
         return true;
     }
@@ -686,9 +709,9 @@
                     values.put(RawContacts.ACCOUNT_NAME, account.name);
                     values.put(RawContacts.ACCOUNT_TYPE, account.type);
 
-                    // Tie this directly to existing aggregate
-                    // TODO: this may need to use aggregation exception rules
-                    final long aggregateId = target.mState.getAggregateId();
+                    // Tie directly to an existing aggregate, which is turned
+                    // into an AggregationException later during persisting.
+                    final long aggregateId = target.mState.getContactId();
                     if (aggregateId >= 0) {
                         values.put(RawContacts.CONTACT_ID, aggregateId);
                     }
@@ -700,6 +723,18 @@
                     final Bundle extras = target.getIntent().getExtras();
                     EntityModifier.parseExtras(target, source, insert, extras);
 
+                    // Ensure we have some default fields
+                    EntityModifier.ensureKindExists(insert, source, Phone.CONTENT_ITEM_TYPE);
+                    EntityModifier.ensureKindExists(insert, source, Email.CONTENT_ITEM_TYPE);
+
+                    // Create "My Contacts" membership for Google contacts
+                    // TODO: move this off into "templates" for each given source
+                    if (HardCodedSources.ACCOUNT_TYPE_GOOGLE.equals(source.accountType)) {
+                        final ValuesDelta membership = HardCodedSources
+                                .buildMyContactsMembership(target);
+                        insert.addEntry(membership);
+                    }
+
                     target.mState.add(insert);
 
                     target.bindTabs();
@@ -716,7 +751,7 @@
                 }
             };
 
-            // TODO: when canceled and single add, finish()
+            // TODO: when canceled and was single add, finish()
             final AlertDialog.Builder builder = new AlertDialog.Builder(target);
             builder.setTitle(R.string.dialog_new_contact_account);
             builder.setSingleChoiceItems(accountAdapter, 0, clickListener);
@@ -762,7 +797,7 @@
      */
     private Dialog createNameDialog() {
         // Build set of all available display names
-        final ArrayList<ValuesDelta> allNames = new ArrayList<ValuesDelta>();
+        final ArrayList<ValuesDelta> allNames = Lists.newArrayList();
         for (EntityDelta entity : mState) {
             final ArrayList<ValuesDelta> displayNames = entity
                     .getMimeEntries(StructuredName.CONTENT_ITEM_TYPE);
diff --git a/src/com/android/contacts/ui/FastTrackWindow.java b/src/com/android/contacts/ui/FastTrackWindow.java
index 5aadcaf..de43c10 100644
--- a/src/com/android/contacts/ui/FastTrackWindow.java
+++ b/src/com/android/contacts/ui/FastTrackWindow.java
@@ -752,10 +752,11 @@
         protected Entry getEntry(Action action) {
             final String mimeType = action.getMimeType();
             Entry entry = mCache.get(mimeType);
-            if (entry == null) {
-                entry = new Entry();
+            if (entry != null) return entry;
+            entry = new Entry();
 
-                final Intent intent = action.getIntent();
+            final Intent intent = action.getIntent();
+            if (intent != null) {
                 final List<ResolveInfo> matches = mPackageManager.queryIntentActivities(intent,
                         PackageManager.MATCH_DEFAULT_ONLY);
 
@@ -766,9 +767,9 @@
                     entry.bestResolve = bestResolve;
                     entry.icon = new SoftReference<Drawable>(icon);
                 }
-
-                mCache.put(mimeType, entry);
             }
+
+            mCache.put(mimeType, entry);
             return entry;
         }
 
diff --git a/src/com/android/contacts/ui/widget/ContactEditorView.java b/src/com/android/contacts/ui/widget/ContactEditorView.java
index 8cbe298..1fc935d 100644
--- a/src/com/android/contacts/ui/widget/ContactEditorView.java
+++ b/src/com/android/contacts/ui/widget/ContactEditorView.java
@@ -42,6 +42,7 @@
 import android.text.Editable;
 import android.text.TextUtils;
 import android.text.TextWatcher;
+import android.util.AttributeSet;
 import android.view.ContextThemeWrapper;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -52,6 +53,7 @@
 import android.widget.EditText;
 import android.widget.ImageView;
 import android.widget.ListAdapter;
+import android.widget.RelativeLayout;
 import android.widget.TextView;
 
 import java.util.List;
@@ -67,11 +69,11 @@
  * adding {@link Data} rows or changing {@link EditType}, are performed through
  * {@link EntityModifier} to ensure that {@link ContactsSource} are enforced.
  */
-public class ContactEditorView extends ViewHolder implements OnClickListener {
-    private static final int RES_CONTENT = R.layout.act_edit_contact;
+public class ContactEditorView extends RelativeLayout implements OnClickListener {
+    private LayoutInflater mInflater;
 
-    private PhotoEditor mPhoto;
-    private StructuredNameEditor mDisplayName;
+    private PhotoEditorView mPhoto;
+    private GenericEditorView mName;
 
     private ViewGroup mGeneral;
     private ViewGroup mSecondary;
@@ -82,25 +84,38 @@
     private Drawable mSecondaryClosed;
 
     public ContactEditorView(Context context) {
-        super(context, RES_CONTENT);
+        super(context);
+    }
 
-        mGeneral = (ViewGroup)mContent.findViewById(R.id.sect_general);
-        mSecondary = (ViewGroup)mContent.findViewById(R.id.sect_secondary);
+    public ContactEditorView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
 
-        mSecondaryHeader = (TextView)mContent.findViewById(R.id.head_secondary);
+    /** {@inheritDoc} */
+    @Override
+    protected void onFinishInflate() {
+        mInflater = (LayoutInflater)getContext().getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+
+        mPhoto = (PhotoEditorView)findViewById(R.id.edit_photo);
+
+        final int photoSize = getResources().getDimensionPixelSize(R.dimen.edit_photo_size);
+
+        mName = (GenericEditorView)findViewById(R.id.edit_name);
+        mName.setMinimumHeight(photoSize);
+        mName.setDeletable(false);
+
+        mGeneral = (ViewGroup)findViewById(R.id.sect_general);
+        mSecondary = (ViewGroup)findViewById(R.id.sect_secondary);
+
+        mSecondaryHeader = (TextView)findViewById(R.id.head_secondary);
         mSecondaryHeader.setOnClickListener(this);
 
-        final Resources res = context.getResources();
+        final Resources res = getResources();
         mSecondaryOpen = res.getDrawable(com.android.internal.R.drawable.expander_ic_maximized);
         mSecondaryClosed = res.getDrawable(com.android.internal.R.drawable.expander_ic_minimized);
 
         this.setSecondaryVisible(false);
-
-        mPhoto = new PhotoEditor(context);
-        mPhoto.swapInto((ViewGroup)mContent.findViewById(R.id.hook_photo));
-
-        mDisplayName = new StructuredNameEditor(context);
-        mDisplayName.swapInto((ViewGroup)mContent.findViewById(R.id.hook_displayname));
     }
 
     /** {@inheritDoc} */
@@ -132,6 +147,9 @@
         // Bail if invalid state or source
         if (state == null || source == null) return;
 
+        // Make sure we have StructuredName
+        EntityModifier.ensureKindExists(state, source, StructuredName.CONTENT_ITEM_TYPE);
+
         // Create editor sections for each possible data kind
         for (DataKind kind : source.getSortedDataKinds()) {
             // Skip kind of not editable
@@ -141,508 +159,21 @@
             if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
                 // Handle special case editor for structured name
                 final ValuesDelta primary = state.getPrimaryEntry(mimeType);
-                mDisplayName.setValues(null, primary, state);
+                mName.setValues(kind, primary, state);
             } else if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
                 // Handle special case editor for photos
                 final ValuesDelta primary = state.getPrimaryEntry(mimeType);
-                mPhoto.setValues(null, primary, state);
+                mPhoto.setValues(kind, primary, state);
             } else {
                 // Otherwise use generic section-based editors
                 if (kind.fieldList == null) continue;
-                final KindSection section = new KindSection(mContext, kind, state);
-                if (kind.secondary) {
-                    mSecondary.addView(section.getView());
-                } else {
-                    mGeneral.addView(section.getView());
-                }
+                final ViewGroup parent = kind.secondary ? mSecondary : mGeneral;
+                final KindSectionView section = (KindSectionView)mInflater.inflate(
+                        R.layout.item_kind_section, parent, false);
+                section.setState(kind, state);
+                section.setId(kind.weight);
+                parent.addView(section);
             }
         }
     }
-
-    /**
-     * Custom view for an entire section of data as segmented by
-     * {@link DataKind} around a {@link Data#MIMETYPE}. This view shows a
-     * section header and a trigger for adding new {@link Data} rows.
-     */
-    protected static class KindSection extends ViewHolder implements OnClickListener,
-            EditorListener {
-        private static final int RES_SECTION = R.layout.item_edit_kind;
-
-        private ViewGroup mEditors;
-        private View mAdd;
-        private TextView mTitle;
-
-        private DataKind mKind;
-        private EntityDelta mState;
-
-        public KindSection(Context context, DataKind kind, EntityDelta state) {
-            super(context, RES_SECTION);
-
-            mContent.setDrawingCacheEnabled(true);
-            ((ViewGroup)mContent).setAlwaysDrawnWithCacheEnabled(true);
-
-            mKind = kind;
-            mState = state;
-
-            mEditors = (ViewGroup)mContent.findViewById(R.id.kind_editors);
-
-            mAdd = mContent.findViewById(R.id.kind_header);
-            mAdd.setOnClickListener(this);
-
-            mTitle = (TextView)mContent.findViewById(R.id.kind_title);
-            mTitle.setText(kind.titleRes);
-
-            this.rebuildFromState();
-            this.updateAddEnabled();
-            this.updateEditorsVisible();
-        }
-
-        public void onDeleted(Editor editor) {
-            this.updateAddEnabled();
-            this.updateEditorsVisible();
-        }
-
-        /**
-         * Build editors for all current {@link #mState} rows.
-         */
-        public void rebuildFromState() {
-            // TODO: build special "stub" entries to help enter first-phone or first-email
-
-            // Remove any existing editors
-            mEditors.removeAllViews();
-
-            // Build individual editors for each entry
-            if (!mState.hasMimeEntries(mKind.mimeType)) return;
-            for (ValuesDelta entry : mState.getMimeEntries(mKind.mimeType)) {
-                // Skip entries that aren't visible
-                if (!entry.isVisible()) continue;
-
-                final GenericEditor editor = new GenericEditor(mContext);
-                editor.setValues(mKind, entry, mState);
-                editor.setEditorListener(this);
-                mEditors.addView(editor.getView());
-            }
-        }
-
-        protected void updateEditorsVisible() {
-            final boolean hasChildren = mEditors.getChildCount() > 0;
-            mEditors.setVisibility(hasChildren ? View.VISIBLE : View.GONE);
-        }
-
-        protected void updateAddEnabled() {
-            // Set enabled state on the "add" view
-            final boolean canInsert = EntityModifier.canInsert(mState, mKind);
-            mAdd.setEnabled(canInsert);
-        }
-
-        public void onClick(View v) {
-            // Insert a new child and rebuild
-            EntityModifier.insertChild(mState, mKind);
-            this.rebuildFromState();
-            this.updateAddEnabled();
-            this.updateEditorsVisible();
-        }
-    }
-
-    /**
-     * Generic definition of something that edits a {@link Data} row through an
-     * {@link ValuesDelta} object.
-     */
-    protected interface Editor {
-        /**
-         * Prepare this editor for the given {@link ValuesDelta}, which
-         * builds any needed views. Any changes performed by the user will be
-         * written back to that same object.
-         */
-        public void setValues(DataKind kind, ValuesDelta values, EntityDelta state);
-
-        /**
-         * Add a specific {@link EditorListener} to this {@link Editor}.
-         */
-        public void setEditorListener(EditorListener listener);
-
-        /**
-         * Called internally when the contents of a specific field have changed,
-         * allowing advanced editors to persist data in a specific way.
-         */
-        public void onFieldChanged(String column, String value);
-    }
-
-    /**
-     * Listener for an {@link Editor}, usually to handle deleted items.
-     */
-    protected interface EditorListener {
-        /**
-         * Called when the given {@link Editor} has been deleted.
-         */
-        public void onDeleted(Editor editor);
-    }
-
-    /**
-     * Simple editor that handles labels and any {@link EditField} defined for
-     * the entry. Uses {@link ValuesDelta} to read any existing
-     * {@link Entity} values, and to correctly write any changes values.
-     */
-    protected static class GenericEditor extends ViewHolder implements Editor, View.OnClickListener {
-        protected static final int RES_EDITOR = R.layout.item_editor;
-        protected static final int RES_FIELD = R.layout.item_editor_field;
-        protected static final int RES_LABEL_ITEM = android.R.layout.simple_list_item_1;
-
-        protected static final int INPUT_TYPE_CUSTOM = EditorInfo.TYPE_CLASS_TEXT
-                | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
-
-        protected TextView mLabel;
-        protected ViewGroup mFields;
-        protected View mDelete;
-
-        protected DataKind mKind;
-        protected ValuesDelta mEntry;
-        protected EntityDelta mState;
-
-        protected EditType mType;
-
-        public GenericEditor(Context context) {
-            super(context, RES_EDITOR);
-
-            mLabel = (TextView)mContent.findViewById(R.id.edit_label);
-            mLabel.setOnClickListener(this);
-
-            mFields = (ViewGroup)mContent.findViewById(R.id.edit_fields);
-
-            mDelete = mContent.findViewById(R.id.edit_delete);
-            mDelete.setOnClickListener(this);
-        }
-
-        protected EditorListener mListener;
-
-        public void setEditorListener(EditorListener listener) {
-            mListener = listener;
-        }
-
-        /**
-         * Build the current label state based on selected {@link EditType} and
-         * possible custom label string.
-         */
-        private void rebuildLabel() {
-            // Handle undetected types
-            if (mType == null) {
-                mLabel.setText(R.string.unknown);
-                return;
-            }
-
-            if (mType.customColumn != null) {
-                // Use custom label string when present
-                final String customText = mEntry.getAsString(mType.customColumn);
-                if (customText != null) {
-                    mLabel.setText(customText);
-                    return;
-                }
-            }
-
-            // Otherwise fall back to using default label
-            mLabel.setText(mType.labelRes);
-        }
-
-        /** {@inheritDoc} */
-        public void onFieldChanged(String column, String value) {
-            // Field changes are saved directly
-            mEntry.put(column, value);
-        }
-
-        /**
-         * Prepare this editor using the given {@link DataKind} for defining
-         * structure and {@link ValuesDelta} describing the content to edit.
-         */
-        public void setValues(DataKind kind, ValuesDelta entry, EntityDelta state) {
-            mKind = kind;
-            mEntry = entry;
-            mState = state;
-
-            if (!entry.isVisible()) {
-                // Hide ourselves entirely if deleted
-                mContent.setVisibility(View.GONE);
-                return;
-            } else {
-                mContent.setVisibility(View.VISIBLE);
-            }
-
-            // Display label selector if multiple types available
-            final boolean hasTypes = EntityModifier.hasEditTypes(kind);
-            mLabel.setVisibility(hasTypes ? View.VISIBLE : View.GONE);
-            if (hasTypes) {
-                mType = EntityModifier.getCurrentType(entry, kind);
-                rebuildLabel();
-            }
-
-            // Build out set of fields
-            mFields.removeAllViews();
-            for (EditField field : kind.fieldList) {
-                // Inflate field from definition
-                EditText fieldView = (EditText)mInflater.inflate(RES_FIELD, mFields, false);
-                if (field.titleRes > 0) {
-                    fieldView.setHint(field.titleRes);
-                }
-                fieldView.setInputType(field.inputType);
-                fieldView.setMinLines(field.minLines);
-
-                // Read current value from state
-                final String column = field.column;
-                final String value = entry.getAsString(column);
-                fieldView.setText(value);
-
-                // Prepare listener for writing changes
-                fieldView.addTextChangedListener(new TextWatcher() {
-                    public void afterTextChanged(Editable s) {
-                        // Trigger event for newly changed value
-                        onFieldChanged(column, s.toString());
-                    }
-
-                    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-                    }
-
-                    public void onTextChanged(CharSequence s, int start, int before, int count) {
-                    }
-                });
-
-                // Hide field when empty and optional value
-                boolean shouldHide = (TextUtils.isEmpty(value) && field.optional);
-                fieldView.setVisibility(shouldHide ? View.GONE : View.VISIBLE);
-
-                mFields.addView(fieldView);
-            }
-        }
-
-        /**
-         * Prepare dialog for entering a custom label.
-         */
-        public Dialog createCustomDialog() {
-            final EditText customType = new EditText(mContext);
-            customType.setInputType(INPUT_TYPE_CUSTOM);
-            customType.requestFocus();
-
-            final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
-            builder.setTitle(R.string.customLabelPickerTitle);
-            builder.setView(customType);
-
-            builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
-                public void onClick(DialogInterface dialog, int which) {
-                    final String customText = customType.getText().toString();
-                    mEntry.put(mType.customColumn, customText);
-                    rebuildLabel();
-                }
-            });
-
-            // TODO: handle canceled case by reverting to previous type?
-            builder.setNegativeButton(android.R.string.cancel, null);
-
-            return builder.create();
-        }
-
-        /**
-         * 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 createLabelDialog() {
-            // Build list of valid types, including the current value
-            final List<EditType> validTypes = EntityModifier.getValidTypes(mState, mKind, mType);
-
-            // Wrap our context to inflate list items using correct theme
-            final Context dialogContext = new ContextThemeWrapper(mContext,
-                    android.R.style.Theme_Light);
-            final LayoutInflater dialogInflater = mInflater.cloneInContext(dialogContext);
-
-            final ListAdapter typeAdapter = new ArrayAdapter<EditType>(mContext, RES_LABEL_ITEM,
-                    validTypes) {
-                @Override
-                public View getView(int position, View convertView, ViewGroup parent) {
-                    if (convertView == null) {
-                        convertView = dialogInflater.inflate(RES_LABEL_ITEM, parent, false);
-                    }
-
-                    final EditType type = this.getItem(position);
-                    final TextView textView = (TextView)convertView;
-                    textView.setText(type.labelRes);
-                    return textView;
-                }
-            };
-
-            final DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() {
-                public void onClick(DialogInterface dialog, int which) {
-                    dialog.dismiss();
-
-                    // User picked type, so write to entry
-                    mType = validTypes.get(which);
-                    mEntry.put(mKind.typeColumn, mType.rawValue);
-
-                    if (mType.customColumn != null) {
-                        // Show custom label dialog if requested by type
-                        createCustomDialog().show();
-                    } else {
-                        rebuildLabel();
-                    }
-                }
-            };
-
-            final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
-            builder.setTitle(R.string.selectLabel);
-            builder.setSingleChoiceItems(typeAdapter, 0, clickListener);
-            return builder.create();
-        }
-
-        /** {@inheritDoc} */
-        public void onClick(View v) {
-            switch (v.getId()) {
-                case R.id.edit_label: {
-                    createLabelDialog().show();
-                    break;
-                }
-                case R.id.edit_delete: {
-                    // Keep around in model, but mark as deleted
-                    mEntry.markDeleted();
-
-                    // Remove editor from parent view
-                    final ViewGroup parent = (ViewGroup)mContent.getParent();
-                    parent.removeView(mContent);
-
-                    if (mListener != null) {
-                        // Notify listener when present
-                        mListener.onDeleted(this);
-                    }
-                    break;
-                }
-            }
-        }
-    }
-
-    /**
-     * Specific editor for {@link StructuredPostal} addresses that flattens any
-     * user changes into {@link StructuredPostal#FORMATTED_ADDRESS} so data
-     * consistency is maintained.
-     */
-    protected static class PostalEditor extends GenericEditor {
-        public PostalEditor(Context context) {
-            super(context);
-        }
-
-        /** {@inheritDoc} */
-        @Override
-        public void onFieldChanged(String column, String value) {
-            super.onFieldChanged(column, value);
-
-            // TODO: flatten the structured values into unstructured
-            mEntry.put(StructuredPostal.FORMATTED_ADDRESS, null);
-        }
-    }
-
-    /**
-     * Simple editor for {@link Photo}.
-     */
-    protected static class PhotoEditor extends ViewHolder implements Editor {
-        private static final int RES_PHOTO = R.layout.item_editor_photo;
-
-        private ImageView mPhoto;
-        private ValuesDelta mEntry;
-
-        public PhotoEditor(Context context) {
-            super(context, RES_PHOTO);
-
-            mPhoto = (ImageView)mContent;
-        }
-
-        /** {@inheritDoc} */
-        public void onFieldChanged(String column, String value) {
-            throw new UnsupportedOperationException("Photos don't support direct field changes");
-        }
-
-        public void setValues(DataKind kind, ValuesDelta values, EntityDelta state) {
-            mEntry = values;
-            if (values != null) {
-                // Try decoding photo if actual entry
-                final byte[] photoBytes = values.getAsByteArray(Photo.PHOTO);
-                if (photoBytes != null) {
-                    final Bitmap photo = BitmapFactory.decodeByteArray(photoBytes, 0,
-                            photoBytes.length);
-
-                    mPhoto.setScaleType(ImageView.ScaleType.CENTER_CROP);
-                    mPhoto.setImageBitmap(photo);
-                } else {
-                    resetDefault();
-                }
-            } else {
-                resetDefault();
-            }
-        }
-
-        protected void resetDefault() {
-            // Invalid photo, show default "add photo" placeholder
-            mPhoto.setScaleType(ImageView.ScaleType.CENTER);
-            mPhoto.setImageResource(R.drawable.ic_menu_add_picture);
-        }
-
-        public void setEditorListener(EditorListener listener) {
-        }
-    }
-
-    /**
-     * Simple editor for {@link StructuredName}.
-     */
-    protected static class StructuredNameEditor extends ViewHolder implements Editor {
-        private static final int RES_DISPLAY_NAME = R.layout.item_editor_displayname;
-
-        private EditText mName;
-        private ValuesDelta mEntry;
-
-        public StructuredNameEditor(Context context) {
-            super(context, RES_DISPLAY_NAME);
-
-            mName = (EditText)mContent.findViewById(R.id.name);
-        }
-
-        /** {@inheritDoc} */
-        public void onFieldChanged(String column, String value) {
-            if (!StructuredName.DISPLAY_NAME.equals(column)) {
-                throw new IllegalArgumentException("StructuredName editor only "
-                        + "supports updating through DISPLAY_NAME field");
-
-            }
-
-            // Field changes are saved directly
-            mEntry.put(column, value);
-
-            // TODO: split into structured fields using NameSplitter
-        }
-
-        private TextWatcher mWatcher = new TextWatcher() {
-            public void afterTextChanged(Editable s) {
-                // Trigger event for newly changed value
-                onFieldChanged(StructuredName.DISPLAY_NAME, s.toString());
-            }
-
-            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-            }
-
-            public void onTextChanged(CharSequence s, int start, int before, int count) {
-            }
-        };
-
-        public void setValues(DataKind kind, ValuesDelta values, EntityDelta state) {
-            mEntry = values;
-            if (values == null) {
-                // Invalid display name, so reset and skip
-                // TODO: should always have a display name?
-                mName.removeTextChangedListener(mWatcher);
-                mName.setText(null);
-                return;
-            }
-
-            final String displayName = values.getAsString(StructuredName.DISPLAY_NAME);
-            mName.removeTextChangedListener(mWatcher);
-            mName.setText(displayName);
-            mName.addTextChangedListener(mWatcher);
-        }
-
-        public void setEditorListener(EditorListener listener) {
-        }
-    }
 }
diff --git a/src/com/android/contacts/ui/widget/GenericEditorView.java b/src/com/android/contacts/ui/widget/GenericEditorView.java
new file mode 100644
index 0000000..3522258
--- /dev/null
+++ b/src/com/android/contacts/ui/widget/GenericEditorView.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.ui.widget;
+
+import com.android.contacts.R;
+import com.android.contacts.model.EntityDelta;
+import com.android.contacts.model.EntityModifier;
+import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.model.ContactsSource.EditField;
+import com.android.contacts.model.ContactsSource.EditType;
+import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.model.Editor;
+import com.android.contacts.model.Editor.EditorListener;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Entity;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.ListAdapter;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import java.util.List;
+
+/**
+ * Simple editor that handles labels and any {@link EditField} defined for
+ * the entry. Uses {@link ValuesDelta} to read any existing
+ * {@link Entity} values, and to correctly write any changes values.
+ */
+public class GenericEditorView extends RelativeLayout implements Editor, View.OnClickListener {
+    protected static final int RES_FIELD = R.layout.item_editor_field;
+    protected static final int RES_LABEL_ITEM = android.R.layout.simple_list_item_1;
+
+    protected LayoutInflater mInflater;
+
+    protected static final int INPUT_TYPE_CUSTOM = EditorInfo.TYPE_CLASS_TEXT
+            | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
+
+    protected TextView mLabel;
+    protected ViewGroup mFields;
+    protected View mDelete;
+    protected View mMore;
+
+    protected DataKind mKind;
+    protected ValuesDelta mEntry;
+    protected EntityDelta mState;
+
+    protected boolean mHideOptional = true;
+
+    protected EditType mType;
+
+    public GenericEditorView(Context context) {
+        super(context);
+    }
+
+    public GenericEditorView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void onFinishInflate() {
+        mInflater = (LayoutInflater)getContext().getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+
+        mLabel = (TextView)findViewById(R.id.edit_label);
+        mLabel.setOnClickListener(this);
+
+        mFields = (ViewGroup)findViewById(R.id.edit_fields);
+
+        mDelete = findViewById(R.id.edit_delete);
+        mDelete.setOnClickListener(this);
+
+        mMore = findViewById(R.id.edit_more);
+        mMore.setOnClickListener(this);
+    }
+
+    protected EditorListener mListener;
+
+    public void setEditorListener(EditorListener listener) {
+        mListener = listener;
+    }
+
+    public void setDeletable(boolean deletable) {
+        mDelete.setVisibility(deletable ? View.VISIBLE : View.INVISIBLE);
+    }
+
+    /**
+     * Build the current label state based on selected {@link EditType} and
+     * possible custom label string.
+     */
+    private void rebuildLabel() {
+        // Handle undetected types
+        if (mType == null) {
+            mLabel.setText(R.string.unknown);
+            return;
+        }
+
+        if (mType.customColumn != null) {
+            // Use custom label string when present
+            final String customText = mEntry.getAsString(mType.customColumn);
+            if (customText != null) {
+                mLabel.setText(customText);
+                return;
+            }
+        }
+
+        // Otherwise fall back to using default label
+        mLabel.setText(mType.labelRes);
+    }
+
+    /** {@inheritDoc} */
+    public void onFieldChanged(String column, String value) {
+        // Field changes are saved directly
+        mEntry.put(column, value);
+    }
+
+    private void rebuildValues() {
+        setValues(mKind, mEntry, mState);
+    }
+
+    /**
+     * Prepare this editor using the given {@link DataKind} for defining
+     * structure and {@link ValuesDelta} describing the content to edit.
+     */
+    public void setValues(DataKind kind, ValuesDelta entry, EntityDelta state) {
+        mKind = kind;
+        mEntry = entry;
+        mState = state;
+
+        if (!entry.isVisible()) {
+            // Hide ourselves entirely if deleted
+            setVisibility(View.GONE);
+            return;
+        } else {
+            setVisibility(View.VISIBLE);
+        }
+
+        // Display label selector if multiple types available
+        final boolean hasTypes = EntityModifier.hasEditTypes(kind);
+        mLabel.setVisibility(hasTypes ? View.VISIBLE : View.GONE);
+        if (hasTypes) {
+            mType = EntityModifier.getCurrentType(entry, kind);
+            rebuildLabel();
+        }
+
+        // Build out set of fields
+        mFields.removeAllViews();
+        boolean hidePossible = false;
+        for (EditField field : kind.fieldList) {
+            // Inflate field from definition
+            EditText fieldView = (EditText)mInflater.inflate(RES_FIELD, mFields, false);
+            if (field.titleRes > 0) {
+                fieldView.setHint(field.titleRes);
+            }
+            fieldView.setInputType(field.inputType);
+            fieldView.setMinLines(field.minLines);
+
+            // Read current value from state
+            final String column = field.column;
+            final String value = entry.getAsString(column);
+            fieldView.setText(value);
+
+            // Prepare listener for writing changes
+            fieldView.addTextChangedListener(new TextWatcher() {
+                public void afterTextChanged(Editable s) {
+                    // Trigger event for newly changed value
+                    onFieldChanged(column, s.toString());
+                }
+
+                public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+                }
+
+                public void onTextChanged(CharSequence s, int start, int before, int count) {
+                }
+            });
+
+            // Hide field when empty and optional value
+            final boolean couldHide = (TextUtils.isEmpty(value) && field.optional);
+            final boolean willHide = (mHideOptional && couldHide);
+            fieldView.setVisibility(willHide ? View.GONE : View.VISIBLE);
+            hidePossible = hidePossible || couldHide;
+
+            mFields.addView(fieldView);
+        }
+
+        // When hiding fields, place expandable
+        mMore.setVisibility(hidePossible ? View.VISIBLE : View.GONE);
+    }
+
+    /**
+     * Prepare dialog for entering a custom label.
+     */
+    public Dialog createCustomDialog() {
+        final EditText customType = new EditText(mContext);
+        customType.setInputType(INPUT_TYPE_CUSTOM);
+        customType.requestFocus();
+
+        final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
+        builder.setTitle(R.string.customLabelPickerTitle);
+        builder.setView(customType);
+
+        builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+            public void onClick(DialogInterface dialog, int which) {
+                final String customText = customType.getText().toString();
+                mEntry.put(mType.customColumn, customText);
+                rebuildLabel();
+            }
+        });
+
+        // TODO: handle canceled case by reverting to previous type?
+        builder.setNegativeButton(android.R.string.cancel, null);
+
+        return builder.create();
+    }
+
+    /**
+     * 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 createLabelDialog() {
+        // Build list of valid types, including the current value
+        final List<EditType> validTypes = EntityModifier.getValidTypes(mState, mKind, mType);
+
+        // Wrap our context to inflate list items using correct theme
+        final Context dialogContext = new ContextThemeWrapper(mContext,
+                android.R.style.Theme_Light);
+        final LayoutInflater dialogInflater = mInflater.cloneInContext(dialogContext);
+
+        final ListAdapter typeAdapter = new ArrayAdapter<EditType>(mContext, RES_LABEL_ITEM,
+                validTypes) {
+            @Override
+            public View getView(int position, View convertView, ViewGroup parent) {
+                if (convertView == null) {
+                    convertView = dialogInflater.inflate(RES_LABEL_ITEM, parent, false);
+                }
+
+                final EditType type = this.getItem(position);
+                final TextView textView = (TextView)convertView;
+                textView.setText(type.labelRes);
+                return textView;
+            }
+        };
+
+        final DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() {
+            public void onClick(DialogInterface dialog, int which) {
+                dialog.dismiss();
+
+                // User picked type, so write to entry
+                mType = validTypes.get(which);
+                mEntry.put(mKind.typeColumn, mType.rawValue);
+
+                if (mType.customColumn != null) {
+                    // Show custom label dialog if requested by type
+                    createCustomDialog().show();
+                } else {
+                    rebuildLabel();
+                }
+            }
+        };
+
+        final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
+        builder.setTitle(R.string.selectLabel);
+        builder.setSingleChoiceItems(typeAdapter, 0, clickListener);
+        return builder.create();
+    }
+
+    /** {@inheritDoc} */
+    public void onClick(View v) {
+        switch (v.getId()) {
+            case R.id.edit_label: {
+                createLabelDialog().show();
+                break;
+            }
+            case R.id.edit_delete: {
+                // Keep around in model, but mark as deleted
+                mEntry.markDeleted();
+
+                // Remove editor from parent view
+                final ViewGroup parent = (ViewGroup)getParent();
+                parent.removeView(this);
+
+                if (mListener != null) {
+                    // Notify listener when present
+                    mListener.onDeleted(this);
+                }
+                break;
+            }
+            case R.id.edit_more: {
+                mHideOptional = !mHideOptional;
+                rebuildValues();
+                break;
+            }
+        }
+    }
+}
diff --git a/src/com/android/contacts/ui/widget/KindSectionView.java b/src/com/android/contacts/ui/widget/KindSectionView.java
new file mode 100644
index 0000000..14ec349
--- /dev/null
+++ b/src/com/android/contacts/ui/widget/KindSectionView.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.ui.widget;
+
+import com.android.contacts.R;
+import com.android.contacts.model.Editor;
+import com.android.contacts.model.EntityDelta;
+import com.android.contacts.model.EntityModifier;
+import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.model.Editor.EditorListener;
+import com.android.contacts.model.EntityDelta.ValuesDelta;
+
+import android.content.Context;
+import android.provider.ContactsContract.Data;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/**
+ * Custom view for an entire section of data as segmented by
+ * {@link DataKind} around a {@link Data#MIMETYPE}. This view shows a
+ * section header and a trigger for adding new {@link Data} rows.
+ */
+public class KindSectionView extends LinearLayout implements OnClickListener, EditorListener {
+    private static final String TAG = "KindSectionView";
+
+    private LayoutInflater mInflater;
+
+    private ViewGroup mEditors;
+    private View mAdd;
+    private TextView mTitle;
+
+    private DataKind mKind;
+    private EntityDelta mState;
+    
+    public KindSectionView(Context context) {
+        super(context);
+    }
+
+    public KindSectionView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void onFinishInflate() {
+        mInflater = (LayoutInflater)getContext().getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+
+        setDrawingCacheEnabled(true);
+        setAlwaysDrawnWithCacheEnabled(true);
+
+        mEditors = (ViewGroup)findViewById(R.id.kind_editors);
+
+        mAdd = findViewById(R.id.kind_header);
+        mAdd.setOnClickListener(this);
+
+        mTitle = (TextView)findViewById(R.id.kind_title);
+    }
+
+    public void onDeleted(Editor editor) {
+        this.updateAddEnabled();
+        this.updateEditorsVisible();
+    }
+
+    public void setState(DataKind kind, EntityDelta state) {
+        mKind = kind;
+        mState = state;
+
+        // TODO: handle resources from remote packages
+        mTitle.setText(kind.titleRes);
+
+        this.rebuildFromState();
+        this.updateAddEnabled();
+        this.updateEditorsVisible();
+    }
+
+    /**
+     * Build editors for all current {@link #mState} rows.
+     */
+    public void rebuildFromState() {
+        // Remove any existing editors
+        mEditors.removeAllViews();
+
+        // Build individual editors for each entry
+        if (!mState.hasMimeEntries(mKind.mimeType)) return;
+        for (ValuesDelta entry : mState.getMimeEntries(mKind.mimeType)) {
+            // Skip entries that aren't visible
+            if (!entry.isVisible()) continue;
+
+            final GenericEditorView editor = (GenericEditorView)mInflater.inflate(
+                    R.layout.item_generic_editor, mEditors, false);
+            editor.setValues(mKind, entry, mState);
+            editor.setEditorListener(this);
+            editor.setId(entry.getViewId());
+            mEditors.addView(editor);
+        }
+    }
+
+    protected void updateEditorsVisible() {
+        final boolean hasChildren = mEditors.getChildCount() > 0;
+        mEditors.setVisibility(hasChildren ? View.VISIBLE : View.GONE);
+    }
+
+    protected void updateAddEnabled() {
+        // Set enabled state on the "add" view
+        final boolean canInsert = EntityModifier.canInsert(mState, mKind);
+        mAdd.setEnabled(canInsert);
+    }
+    
+    public void onClick(View v) {
+        // Insert a new child and rebuild
+        EntityModifier.insertChild(mState, mKind);
+        this.rebuildFromState();
+        this.updateAddEnabled();
+        this.updateEditorsVisible();
+    }
+}
diff --git a/src/com/android/contacts/ui/widget/PhotoEditorView.java b/src/com/android/contacts/ui/widget/PhotoEditorView.java
new file mode 100644
index 0000000..b88dc3b
--- /dev/null
+++ b/src/com/android/contacts/ui/widget/PhotoEditorView.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.ui.widget;
+
+import com.android.contacts.R;
+import com.android.contacts.model.EntityDelta;
+import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.model.Editor;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+/**
+ * Simple editor for {@link Photo}.
+ */
+public class PhotoEditorView extends ImageView implements Editor {
+    private ValuesDelta mEntry;
+
+    public PhotoEditorView(Context context) {
+        super(context);
+    }
+
+    public PhotoEditorView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    /** {@inheritDoc} */
+    public void onFieldChanged(String column, String value) {
+        throw new UnsupportedOperationException("Photos don't support direct field changes");
+    }
+
+    public void setValues(DataKind kind, ValuesDelta values, EntityDelta state) {
+        mEntry = values;
+        if (values != null) {
+            // Try decoding photo if actual entry
+            final byte[] photoBytes = values.getAsByteArray(Photo.PHOTO);
+            if (photoBytes != null) {
+                final Bitmap photo = BitmapFactory.decodeByteArray(photoBytes, 0,
+                        photoBytes.length);
+
+                setScaleType(ImageView.ScaleType.CENTER_CROP);
+                setImageBitmap(photo);
+            } else {
+                resetDefault();
+            }
+        } else {
+            resetDefault();
+        }
+    }
+
+    protected void resetDefault() {
+        // Invalid photo, show default "add photo" placeholder
+        setScaleType(ImageView.ScaleType.CENTER);
+        setImageResource(R.drawable.ic_menu_add_picture);
+    }
+
+    public void setEditorListener(EditorListener listener) {
+    }
+}
diff --git a/src/com/android/contacts/ui/widget/ViewHolder.java b/src/com/android/contacts/ui/widget/ViewHolder.java
deleted file mode 100644
index 241dddd..0000000
--- a/src/com/android/contacts/ui/widget/ViewHolder.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.contacts.ui.widget;
-
-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.
- */
-public class ViewHolder {
-    protected Context mContext;
-    protected LayoutInflater mInflater;
-    protected View mContent;
-
-    public ViewHolder(Context context, int layoutRes) {
-        mContext = context;
-        mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-        mContent = mInflater.inflate(layoutRes, null);
-    }
-
-    public void swapInto(ViewGroup target) {
-        target.removeAllViews();
-        target.addView(mContent);
-    }
-
-    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;
-    }
-}