Add a multi account KindSectionView (E4)

* This allows us to treat all kinds (i.e. mime types) the
  same (except for name and photo).
* We can also set the icons and add empty editors naturally
  (w/o checking if the kind section is the last on in the list)
* Finally, we can also expand to the full editor in place now

Bug 23589603

Change-Id: Ibdecd8a9ef33cd1db70ffd9fd17e7883555d1fa4
diff --git a/src/com/android/contacts/editor/CompactContactEditorFragment.java b/src/com/android/contacts/editor/CompactContactEditorFragment.java
index 287d814..df47fe4 100644
--- a/src/com/android/contacts/editor/CompactContactEditorFragment.java
+++ b/src/com/android/contacts/editor/CompactContactEditorFragment.java
@@ -159,7 +159,7 @@
 
         final View view = inflater.inflate(
                 R.layout.compact_contact_editor_fragment, container, false);
-        mContent = (LinearLayout) view.findViewById(R.id.editors);
+        mContent = (LinearLayout) view.findViewById(R.id.raw_contacts_editor_view);
         return view;
     }
 
@@ -350,30 +350,6 @@
     }
 
     @Override
-    public void onExpandEditor() {
-        // Determine if this is an insert (new contact) or edit
-        final boolean isInsert = isInsert(getActivity().getIntent());
-
-        if (isInsert) {
-            // For inserts, prevent any changes from being saved when the base fragment is destroyed
-            mStatus = Status.CLOSING;
-        } else if (hasPendingRawContactChanges()) {
-            // Save whatever is in the form
-            save(SaveMode.CLOSE, /* backPressed =*/ false);
-        }
-
-        // Prepare an Intent to start the expanded editor
-        final Intent intent = isInsert
-                ? EditorIntents.createInsertContactIntent(mState, getDisplayName(),
-                        getPhoneticName(), mUpdatedPhotos, mNewLocalProfile)
-                : EditorIntents.createEditContactIntent(mLookupUri, getMaterialPalette(),
-                        mPhotoId, mNameId);
-        ImplicitIntentsUtil.startActivityInApp(getActivity(), intent);
-
-        getActivity().finish();
-    }
-
-    @Override
     public void onNameFieldChanged(long rawContactId, ValuesDelta valuesDelta) {
         final Activity activity = getActivity();
         if (activity == null || activity.isFinishing()) {
diff --git a/src/com/android/contacts/editor/CompactKindSectionView.java b/src/com/android/contacts/editor/CompactKindSectionView.java
new file mode 100644
index 0000000..827eb58
--- /dev/null
+++ b/src/com/android/contacts/editor/CompactKindSectionView.java
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.editor;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+import com.android.contacts.R;
+import com.android.contacts.common.model.RawContactDelta;
+import com.android.contacts.common.model.RawContactModifier;
+import com.android.contacts.common.model.ValuesDelta;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.editor.Editor.EditorListener;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Version of {@link KindSectionView} that supports multiple RawContactDeltas.
+ */
+public class CompactKindSectionView extends LinearLayout implements EditorListener {
+
+    private ViewGroup mEditors;
+    private ImageView mIcon;
+
+    private List<KindSectionData> mKindSectionDatas;
+    private boolean mReadOnly;
+    private boolean mShowOneEmptyEditor = false;
+    private boolean mHideIfEmpty = true;
+
+    private ViewIdGenerator mViewIdGenerator;
+    private LayoutInflater mInflater;
+
+    public CompactKindSectionView(Context context) {
+        this(context, /** attrs =*/ null);
+    }
+
+    public CompactKindSectionView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        super.setEnabled(enabled);
+        if (mEditors != null) {
+            int childCount = mEditors.getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                mEditors.getChildAt(i).setEnabled(enabled);
+            }
+        }
+        // TODO: why is this necessary?
+        updateEmptyEditors(/* shouldAnimate = */ true);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void onFinishInflate() {
+        setDrawingCacheEnabled(true);
+        setAlwaysDrawnWithCacheEnabled(true);
+
+        mInflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+        mEditors = (ViewGroup) findViewById(R.id.kind_editors);
+        mIcon = (ImageView) findViewById(R.id.kind_icon);
+    }
+
+    @Override
+    public void onDeleteRequested(Editor editor) {
+        if (mShowOneEmptyEditor && getEditorCount() == 1) {
+            // If there is only 1 editor in the section, then don't allow the user to delete it.
+            // Just clear the fields in the editor.
+            editor.clearAllFields();
+        } else {
+            editor.deleteEditor();
+        }
+    }
+
+    @Override
+    public void onRequest(int request) {
+        // If a field has become empty or non-empty, then check if another row
+        // can be added dynamically.
+        if (request == FIELD_TURNED_EMPTY || request == FIELD_TURNED_NON_EMPTY) {
+            updateEmptyEditors(/* shouldAnimate = */ true);
+        }
+    }
+
+    /**
+     * @param showOneEmptyEditor If true, we will always show one empty, otherwise an empty editor
+     *         will not be shown until the user enters a value.
+     */
+    public void setShowOneEmptyEditor(boolean showOneEmptyEditor) {
+        mShowOneEmptyEditor = showOneEmptyEditor;
+    }
+
+    /**
+     * @param hideWhenEmpty If true, the entire section will be hidden if all inputs are empty,
+     *         otherwise one empty input will always be displayed.
+     */
+    public void setHideWhenEmpty(boolean hideWhenEmpty) {
+        mHideIfEmpty = hideWhenEmpty;
+    }
+
+    public void setState(List<KindSectionData> kindSectionDatas, boolean readOnly,
+            ViewIdGenerator viewIdGenerator) {
+        mKindSectionDatas = kindSectionDatas;
+        mReadOnly = readOnly;
+        mViewIdGenerator = viewIdGenerator;
+
+        // Set the icon using the first DataKind (all DataKinds should be the same type)
+        final DataKind dataKind = mKindSectionDatas.isEmpty()
+                ? null : mKindSectionDatas.get(0).getDataKind();
+        if (dataKind != null) {
+            mIcon.setContentDescription(dataKind.titleRes == -1 || dataKind.titleRes == 0
+                    ? "" : getResources().getString(dataKind.titleRes));
+            mIcon.setImageDrawable(EditorUiUtils.getMimeTypeDrawable(getContext(),
+                    dataKind.mimeType));
+            if (mIcon.getDrawable() == null) mIcon.setContentDescription(null);
+        }
+
+        rebuildFromState();
+        updateEmptyEditors(/* shouldAnimate = */ false);
+    }
+
+    /**
+     * Build editors for all current rows.
+     */
+    private void rebuildFromState() {
+        // Remove any existing editors
+        mEditors.removeAllViews();
+
+        // Check if we are displaying anything here
+        boolean hasValuesDeltas = false;
+        for (KindSectionData kindSectionData : mKindSectionDatas) {
+            if (kindSectionData.hasValuesDeltas()) {
+                hasValuesDeltas = true;
+                break;
+            }
+        }
+
+        if (hasValuesDeltas) {
+            for (KindSectionData kindSectionData : mKindSectionDatas) {
+                for (ValuesDelta valuesDelta : kindSectionData.getValuesDeltas()) {
+                    // Skip entries that aren't visible
+                    if (!valuesDelta.isVisible()) continue;
+                    if (isUnchanged(kindSectionData.getDataKind(), valuesDelta)) continue;
+                    createEditorView(kindSectionData.getRawContactDelta(),
+                            kindSectionData.getDataKind(), valuesDelta);
+                }
+            }
+        }
+    }
+
+    /**
+     * Creates an EditorView for the given values delta. This function must be used while
+     * constructing the views corresponding to the the object-model. The resulting EditorView is
+     * also added to the end of mEditors
+     */
+    private View createEditorView(RawContactDelta rawContactDelta, DataKind dataKind,
+            ValuesDelta entry) {
+        // Inflate the layout
+        final View view;
+        final int layoutResId = EditorUiUtils.getLayoutResourceId(dataKind.mimeType);
+        try {
+            view = mInflater.inflate(layoutResId, mEditors, false);
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to allocate editor with layout resource ID " +
+                    layoutResId + " for mime type " + dataKind.mimeType + ": " + e.toString());
+        }
+
+        // Hide the types drop downs until the associated edit field is focused
+        if (view instanceof LabeledEditorView) {
+            ((LabeledEditorView) view).setHideTypeInitially(true);
+        }
+
+        // Fix the start margin for phonetic name views
+        if (view instanceof PhoneticNameEditorView) {
+            final LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
+                    LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+            layoutParams.setMargins(0, 0, 0, 0);
+            view.setLayoutParams(layoutParams);
+        }
+
+        // Set whether the editor is enabled
+        view.setEnabled(isEnabled());
+
+        if (view instanceof Editor) {
+            Editor editor = (Editor) view;
+            editor.setDeletable(true);
+            editor.setValues(dataKind, entry, rawContactDelta, !dataKind.editable, mViewIdGenerator);
+            editor.setEditorListener(this);
+        }
+        mEditors.addView(view);
+        return view;
+    }
+
+    /**
+     * Whether the given values delta has no changes (i.e. it exists in the database but is empty).
+     */
+    private static boolean isUnchanged(DataKind dataKind, ValuesDelta item) {
+        if (!item.isNoop()) return false;
+        final int fieldCount = dataKind.fieldList == null ? 0 : dataKind.fieldList.size();
+        for (int i = 0; i < fieldCount; i++) {
+            final String column = dataKind.fieldList.get(i).column;
+            final String value = item.getAsString(column);
+            if (!TextUtils.isEmpty(value)) return false;
+        }
+        return true;
+    }
+
+    /**
+     * Updates the editors being displayed to the user removing extra empty
+     * {@link Editor}s, so there is only max 1 empty {@link Editor} view at a time.
+     * If there is only 1 empty editor and {@link #setHideWhenEmpty} was set to true,
+     * then the entire section is hidden.
+     */
+    public void updateEmptyEditors(boolean shouldAnimate) {
+        if (mKindSectionDatas.isEmpty()) return;
+        final DataKind dataKind = mKindSectionDatas.get(0).getDataKind();
+        final RawContactDelta rawContactDelta = mKindSectionDatas.get(0).getRawContactDelta();
+
+        // Update whether the entire section is visible or not
+        final int editorCount = getEditorCount();
+        final List<View> emptyEditors = getEmptyEditors();
+        if (editorCount == emptyEditors.size() && mHideIfEmpty) {
+            setVisibility(GONE);
+            return;
+        }
+        setVisibility(VISIBLE);
+
+        // Update the number of empty editors
+        if (emptyEditors.size() > 1) {
+            // If there is more than 1 empty editor, then remove it from the list of editors.
+            int deleted = 0;
+            for (final View emptyEditorView : emptyEditors) {
+                // If no child {@link View}s are being focused on within this {@link View}, then
+                // remove this empty editor. We can assume that at least one empty editor has focus.
+                // One way to get two empty editors is by deleting characters from a non-empty
+                // editor, in which case this editor has focus.  Another way is if there is more
+                // values delta so we must also count number of editors deleted.
+
+                // TODO: we must not delete the editor for the "primary" account. It's working
+                // because the primary account is always the last one when the account is changed
+                // in the editor but it is a bit brittle to rely on that (though that is what is
+                // happening in LMP).
+                if (emptyEditorView.findFocus() == null) {
+                    final Editor editor = (Editor) emptyEditorView;
+                    if (shouldAnimate) {
+                        editor.deleteEditor();
+                    } else {
+                        mEditors.removeView(emptyEditorView);
+                    }
+                    deleted++;
+                    if (deleted == emptyEditors.size() -1) break;
+                }
+            }
+            return;
+        }
+        if (dataKind == null // There is nothing we can do.
+                || mReadOnly // We don't show empty editors for read only data kinds.
+                // We have already reached the maximum number of editors, don't add any more.
+                || (dataKind.typeOverallMax == editorCount && dataKind.typeOverallMax != 0)
+                // We have already reached the maximum number of empty editors, don't add any more.
+                || emptyEditors.size() == 1) {
+            return;
+        }
+        // Add a new empty editor
+        if (mShowOneEmptyEditor) {
+            final ValuesDelta values = RawContactModifier.insertChild(rawContactDelta, dataKind);
+            final View editorView = createEditorView(rawContactDelta, dataKind, values);
+            if (shouldAnimate) {
+                editorView.setVisibility(View.GONE);
+                EditorAnimator.getInstance().showFieldFooter(editorView);
+            }
+        }
+    }
+
+    /**
+     * Returns a list of empty editor views in this section.
+     */
+    private List<View> getEmptyEditors() {
+        List<View> emptyEditorViews = new ArrayList<View>();
+        for (int i = 0; i < mEditors.getChildCount(); i++) {
+            View view = mEditors.getChildAt(i);
+            if (((Editor) view).isEmpty()) {
+                emptyEditorViews.add(view);
+            }
+        }
+        return emptyEditorViews;
+    }
+
+    private int getEditorCount() {
+        return mEditors.getChildCount();
+    }
+
+    /**
+     * Returns the editor View at the given index.
+     */
+    public View getEditorView(int index) {
+        return mEditors == null || mEditors.getChildCount() == 0 || mEditors.getChildCount() > index
+                ? null : mEditors.getChildAt(index);
+    }
+}
diff --git a/src/com/android/contacts/editor/CompactRawContactsEditorView.java b/src/com/android/contacts/editor/CompactRawContactsEditorView.java
index 4aabf0e..c51e47a 100644
--- a/src/com/android/contacts/editor/CompactRawContactsEditorView.java
+++ b/src/com/android/contacts/editor/CompactRawContactsEditorView.java
@@ -23,7 +23,6 @@
 import com.android.contacts.common.model.RawContactModifier;
 import com.android.contacts.common.model.ValuesDelta;
 import com.android.contacts.common.model.account.AccountType;
-import com.android.contacts.common.model.account.AccountType.EditField;
 import com.android.contacts.common.model.account.AccountWithDataSet;
 import com.android.contacts.common.model.dataitem.DataKind;
 import com.android.contacts.common.util.AccountsListAdapter;
@@ -35,11 +34,19 @@
 import android.graphics.Bitmap;
 import android.net.Uri;
 import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Im;
 import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Log;
@@ -53,30 +60,29 @@
 import android.widget.TextView;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.TreeSet;
 
 /**
- * View to display information from multiple {@link RawContactDelta}s grouped together
- * (e.g. all the phone numbers from a {@link com.android.contacts.common.model.Contact} together.
+ * View to display information from multiple {@link RawContactDelta}s grouped together.
  */
 public class CompactRawContactsEditorView extends LinearLayout implements View.OnClickListener {
 
     private static final String TAG = "CompactEditorView";
 
+    private static final MimeTypeComparator MIME_TYPE_COMPARATOR = new MimeTypeComparator();
+
     /**
      * Callbacks for hosts of {@link CompactRawContactsEditorView}s.
      */
     public interface Listener {
 
         /**
-         * Invoked when the compact editor should be expanded to show all fields.
-         */
-        public void onExpandEditor();
-
-        /**
          * Invoked when the structured name editor field has changed.
          *
          * @param rawContactId The raw contact ID from the underlying {@link RawContactDelta}.
@@ -135,6 +141,55 @@
         }
     }
 
+    /** Used to sort kind sections. */
+    private static final class MimeTypeComparator implements
+            Comparator<Map.Entry<String,List<KindSectionData>>> {
+
+        // Order of kinds roughly matches quick contacts; we diverge in the following ways:
+        // 1) all names are together at the top (in quick contacts the nickname and phonetic name
+        //    are in the about card)
+        // 2) IM is moved up after addresses
+        // 3) SIP addresses are moved to below phone numbers
+        private static final List<String> MIME_TYPE_ORDER = Arrays.asList(new String[] {
+                StructuredName.CONTENT_ITEM_TYPE,
+                DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME,
+                DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME,
+                Nickname.CONTENT_ITEM_TYPE,
+                Phone.CONTENT_ITEM_TYPE,
+                SipAddress.CONTENT_ITEM_TYPE,
+                Email.CONTENT_ITEM_TYPE,
+                StructuredPostal.CONTENT_ITEM_TYPE,
+                Im.CONTENT_ITEM_TYPE,
+                Website.CONTENT_ITEM_TYPE,
+                Organization.CONTENT_ITEM_TYPE,
+                Event.CONTENT_ITEM_TYPE,
+                Relation.CONTENT_ITEM_TYPE,
+                Note.CONTENT_ITEM_TYPE
+        });
+
+        @Override
+        public int compare(Map.Entry<String, List<KindSectionData>> entry1,
+                Map.Entry<String, List<KindSectionData>> entry2) {
+            if (entry1 == entry2) return 0;
+            if (entry1 == null) return -1;
+            if (entry2 == null) return 1;
+
+            final String mimeType1 = entry1.getKey();
+            final String mimeType2 = entry2.getKey();
+
+            int index1 = MIME_TYPE_ORDER.indexOf(mimeType1);
+            int index2 = MIME_TYPE_ORDER.indexOf(mimeType2);
+
+            // Fallback to alphabetical ordering of the mime type if both are not found
+            if (index1 < 0 && index2 < 0) return mimeType1.compareTo(mimeType2);
+            if (index1 < 0) return 1;
+            if (index2 < 0) return -1;
+
+            return index1 < index2 ? -1 : 1;
+        }
+    }
+
+
     private Listener mListener;
 
     private AccountTypeManager mAccountTypeManager;
@@ -164,12 +219,9 @@
 
     private CompactPhotoEditorView mPhoto;
     private ViewGroup mNames;
-    private ViewGroup mPhoneticNames;
-    private ViewGroup mNicknames;
-    private ViewGroup mPhoneNumbers;
-    private ViewGroup mEmails;
-    private ViewGroup mOtherTypes;
-    private Map<String,LinearLayout> mOtherTypesMap = new HashMap<>();
+    private ViewGroup mKindSectionViews;
+    // Map of mime types to KindSectionViews
+    private Map<String,LinearLayout> mKindSectionViewsMap = new HashMap<>();
     private View mMoreFields;
 
     // The ValuesDelta for the non super primary name that was displayed to the user.
@@ -216,19 +268,24 @@
 
         mPhoto = (CompactPhotoEditorView) findViewById(R.id.photo_editor);
         mNames = (LinearLayout) findViewById(R.id.names);
-        mPhoneticNames = (LinearLayout) findViewById(R.id.phonetic_names);
-        mNicknames = (LinearLayout) findViewById(R.id.nicknames);
-        mPhoneNumbers = (LinearLayout) findViewById(R.id.phone_numbers);
-        mEmails = (LinearLayout) findViewById(R.id.emails);
-        mOtherTypes = (LinearLayout) findViewById(R.id.other);
+        mKindSectionViews = (LinearLayout) findViewById(R.id.kind_section_views);
         mMoreFields = findViewById(R.id.more_fields);
         mMoreFields.setOnClickListener(this);
     }
 
     @Override
     public void onClick(View view) {
-        if (view.getId() == R.id.more_fields && mListener != null ) {
-            mListener.onExpandEditor();
+        if (view.getId() == R.id.more_fields) {
+            // Stop hiding empty editors and allow the user to enter values for all kinds now
+            for (int i = 0; i < mKindSectionViews.getChildCount(); i++) {
+                final CompactKindSectionView kindSectionView =
+                        (CompactKindSectionView) mKindSectionViews.getChildAt(i);
+                kindSectionView.setHideWhenEmpty(false);
+                kindSectionView.setShowOneEmptyEditor(true);
+                kindSectionView.updateEmptyEditors(/* shouldAnimate =*/ false);
+            }
+
+            updateMoreFieldsButton();
         }
     }
 
@@ -236,13 +293,7 @@
     public void setEnabled(boolean enabled) {
         super.setEnabled(enabled);
         setEnabled(enabled, mNames);
-        setEnabled(enabled, mPhoneticNames);
-        setEnabled(enabled, mNicknames);
-        setEnabled(enabled, mPhoneNumbers);
-        setEnabled(enabled, mEmails);
-        for (Map.Entry<String,LinearLayout> otherType : mOtherTypesMap.entrySet()) {
-            setEnabled(enabled, otherType.getValue());
-        }
+        setEnabled(enabled, mKindSectionViews);
     }
 
     private void setEnabled(boolean enabled, ViewGroup viewGroup) {
@@ -300,9 +351,16 @@
     }
 
     public PhoneticNameEditorView getFirstPhoneticNameEditorView() {
-        // There should only ever be one phonetic name
-        return mPhoneticNames.getChildCount() == 0
-                ? null : (PhoneticNameEditorView) mPhoneticNames.getChildAt(0);
+        // There should only ever be one phonetic name, it is enforced by dataKind.typeOverallMax
+        final CompactKindSectionView kindSectionView = (CompactKindSectionView)
+                mKindSectionViewsMap.get(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME);
+        if (kindSectionView != null && kindSectionView.getChildCount() > 0) {
+            final View editorView = kindSectionView.getEditorView(0);
+            if (editorView instanceof PhoneticNameEditorView) {
+                return ((PhoneticNameEditorView) editorView);
+            }
+        }
+        return null;
     }
 
     public View getAggregationAnchorView() {
@@ -325,12 +383,8 @@
         mKindSectionDataMap.clear();
 
         mNames.removeAllViews();
-        mPhoneticNames.removeAllViews();
-        mNicknames.removeAllViews();
-        mPhoneNumbers.removeAllViews();
-        mEmails.removeAllViews();
-        mOtherTypes.removeAllViews();
-        mOtherTypesMap.clear();
+        mKindSectionViews.removeAllViews();
+        mKindSectionViewsMap.clear();
         mMoreFields.setVisibility(View.VISIBLE);
 
         if (rawContactDeltas == null || rawContactDeltas.isEmpty()) {
@@ -352,19 +406,19 @@
         }
         vlog("state: primary " + mPrimaryAccount);
 
-        vlog("state: setting compact editor state from " + rawContactDeltas);
+
+        vlog("state: setting editor state from " + rawContactDeltas.size() + " RawContactDelta(s)");
         parseRawContactDeltas(rawContactDeltas);
+        if (mKindSectionDataMap == null || mKindSectionDataMap.isEmpty()) {
+            elog("No kind section data parsed from raw contact deltas");
+            return;
+        }
         addAccountInfo();
         addPhotoView();
-        addStructuredNameView();
-        addEditorViews(rawContactDeltas);
-        updateKindEditorEmptyFields(mPhoneNumbers);
-        updateKindEditorIcons(mPhoneNumbers);
-        updateKindEditorEmptyFields(mEmails);
-        updateKindEditorIcons(mEmails);
-        for (Map.Entry<String,LinearLayout> otherTypes : mOtherTypesMap.entrySet()) {
-            updateKindEditorIcons(otherTypes.getValue());
-        }
+        addNameView();
+        addKindSectionViews();
+
+        updateMoreFieldsButton();
     }
 
     private void parseRawContactDeltas(RawContactDeltaList rawContactDeltas) {
@@ -563,8 +617,8 @@
                 mViewIdGenerator);
     }
 
-    private void addStructuredNameView() {
-        // Get the kind section data and values delta that will back the name view
+    private void addNameView() {
+        // Get the kind section data and values delta that will back the photo view
         final String mimeType = StructuredName.CONTENT_ITEM_TYPE;
         Pair<KindSectionData,ValuesDelta> pair = getPrimaryKindSectionData(mimeType, mNameId);
         if (pair == null) {
@@ -578,7 +632,6 @@
         // read-only contact in the name editor backed by the new raw contact
         // that was created. The new raw contact is the first writable one.
         // See go/editing-read-only-contacts
-        // TODO
         if (!TextUtils.isEmpty(mReadOnlyDisplayName)) {
             for (KindSectionData data : mKindSectionDataMap.get(mimeType)) {
                 if (data.getAccountType().areContactsWritable()) {
@@ -658,197 +711,37 @@
                 ? new Pair<>(resultKindSectionData, resultValuesDelta) : null;
     }
 
-    private void addEditorViews(RawContactDeltaList rawContactDeltas) {
-        for (RawContactDelta rawContactDelta : rawContactDeltas) {
-            if (!rawContactDelta.isVisible()) continue;
-            final AccountType accountType = rawContactDelta.getAccountType(mAccountTypeManager);
+    private void addKindSectionViews() {
+        // Sort the kinds
+        final TreeSet<Map.Entry<String,List<KindSectionData>>> entries =
+                new TreeSet<>(MIME_TYPE_COMPARATOR);
+        entries.addAll(mKindSectionDataMap.entrySet());
 
-            for (DataKind dataKind : accountType.getSortedDataKinds()) {
-                if (!dataKind.editable) continue;
+        vlog("kind: " + entries.size() + " kindSection(s)");
+        int i = 0;
+        for (Map.Entry<String, List<KindSectionData>> entry : entries) {
+            final String mimeType = entry.getKey();
+            final List<KindSectionData> kindSectionDataList = entry.getValue();
+            vlog("kind: " + i++ + " " + mimeType + ": " + (kindSectionDataList == null ? 0
+                    : kindSectionDataList.size()) + " kindSectionData(s)");
 
-                final String mimeType = dataKind.mimeType;
-                vlog(mimeType + " " + dataKind.fieldList.size() + " field(s)");
-                if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)
-                        || StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)
-                        || GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
-                    // Photos and structured names are handled separately and
-                    // group membership is not supported
-                    continue;
-                } else if (DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(mimeType)) {
-                    // Only add phonetic names if there is a non-empty one. Note the use of
-                    // StructuredName mimeType below, even though we matched a pseudo mime type.
-                    final ValuesDelta valuesDelta = rawContactDelta.getSuperPrimaryEntry(
-                            StructuredName.CONTENT_ITEM_TYPE, /* forceSelection =*/ true);
-                    if (hasNonEmptyValue(dataKind, valuesDelta)) {
-                        mPhoneticNames.addView(inflatePhoneticNameEditorView(
-                                mPhoneticNames, accountType, valuesDelta, rawContactDelta));
-                    }
-                } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) {
-                    // Add all non-empty nicknames
-                    final List<ValuesDelta> valuesDeltas = getNonEmptyValuesDeltas(
-                            rawContactDelta, Nickname.CONTENT_ITEM_TYPE, dataKind);
-                    if (valuesDeltas != null && !valuesDeltas.isEmpty()) {
-                        for (ValuesDelta valuesDelta : valuesDeltas) {
-                            mNicknames.addView(inflateNicknameEditorView(
-                                    mNicknames, dataKind, valuesDelta, rawContactDelta));
-                        }
-                    }
-                } else if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
-                    final KindSectionView kindSectionView =
-                            inflateKindSectionView(mPhoneNumbers, dataKind, rawContactDelta);
-                    kindSectionView.setListener(new KindSectionView.Listener() {
-                        @Override
-                        public void onDeleteRequested(Editor editor) {
-                            if (kindSectionView.getEditorCount() == 1) {
-                                kindSectionView.markForRemoval();
-                                EditorAnimator.getInstance().removeEditorView(kindSectionView);
-                            } else {
-                                editor.deleteEditor();
-                            }
-                            updateKindEditorEmptyFields(mPhoneNumbers);
-                            updateKindEditorIcons(mPhoneNumbers);
-                        }
-                    });
-                    mPhoneNumbers.addView(kindSectionView);
-                } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
-                    final KindSectionView kindSectionView =
-                            inflateKindSectionView(mEmails, dataKind, rawContactDelta);
-                    kindSectionView.setListener(new KindSectionView.Listener() {
-                        @Override
-                        public void onDeleteRequested(Editor editor) {
-                            if (kindSectionView.getEditorCount() == 1) {
-                                kindSectionView.markForRemoval();
-                                EditorAnimator.getInstance().removeEditorView(kindSectionView);
-                            } else {
-                                editor.deleteEditor();
-                            }
-                            updateKindEditorEmptyFields(mEmails);
-                            updateKindEditorIcons(mEmails);
-                        }
-                    });
-                    mEmails.addView(kindSectionView);
-                } else if (hasNonEmptyValuesDelta(rawContactDelta, mimeType, dataKind)) {
-                    final LinearLayout otherTypeViewGroup;
-                    if (mOtherTypesMap.containsKey(mimeType)) {
-                        otherTypeViewGroup = mOtherTypesMap.get(mimeType);
-                    } else {
-                        otherTypeViewGroup = new LinearLayout(getContext());
-                        otherTypeViewGroup.setOrientation(LinearLayout.VERTICAL);
-                        mOtherTypes.addView(otherTypeViewGroup);
-                        mOtherTypesMap.put(mimeType, otherTypeViewGroup);
-                    }
-                    final KindSectionView kindSectionView =
-                            inflateKindSectionView(mOtherTypes, dataKind, rawContactDelta);
-                    kindSectionView.setListener(new KindSectionView.Listener() {
-                        @Override
-                        public void onDeleteRequested(Editor editor) {
-                            if (kindSectionView.getEditorCount() == 1) {
-                                kindSectionView.markForRemoval();
-                                EditorAnimator.getInstance().removeEditorView(kindSectionView);
-                            } else {
-                                editor.deleteEditor();
-                            }
-                            updateKindEditorIcons(otherTypeViewGroup);
-                        }
-                    });
-                    otherTypeViewGroup.addView(kindSectionView);
-                }
+            // Ignore mime types that we've already handled
+            if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)
+                    || StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)
+                    || DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME.equals(mimeType)) continue;
+
+            // Ignore mime types that we don't handle
+            if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) continue;
+
+            if (kindSectionDataList != null) {
+                final CompactKindSectionView kindSectionView = inflateKindSectionView(
+                        mKindSectionViews, kindSectionDataList, mimeType);
+                mKindSectionViewsMap.put(mimeType, kindSectionView);
+                mKindSectionViews.addView(kindSectionView);
             }
         }
     }
 
-    private static void updateKindEditorEmptyFields(ViewGroup viewGroup) {
-        KindSectionView lastVisibleKindSectionView = null;
-        for (int i = 0; i < viewGroup.getChildCount(); i++) {
-            if (viewGroup.getChildAt(i).getVisibility() == View.VISIBLE) {
-                lastVisibleKindSectionView = (KindSectionView) viewGroup.getChildAt(i);
-            }
-        }
-        // Only the last editor should show an empty editor
-        if (lastVisibleKindSectionView != null) {
-            // Hide all empty kind sections except the last one
-            for (int i = 0; i < viewGroup.getChildCount(); i++) {
-                final KindSectionView kindSectionView = (KindSectionView) viewGroup.getChildAt(i);
-                if (kindSectionView != lastVisibleKindSectionView
-                        && kindSectionView.areAllEditorsEmpty()) {
-                    kindSectionView.setVisibility(View.GONE);
-                }
-            }
-            // Set the last editor to show empty editor fields
-            lastVisibleKindSectionView.setShowOneEmptyEditor(true);
-            lastVisibleKindSectionView.updateEmptyEditors(/* shouldAnimate =*/ false);
-        }
-    }
-
-    private static void updateKindEditorIcons(ViewGroup viewGroup) {
-        // Show the icon on the first visible kind editor
-        boolean iconVisible = false;
-        for (int i = 0; i < viewGroup.getChildCount(); i++) {
-            final KindSectionView kindSectionView = (KindSectionView) viewGroup.getChildAt(i);
-            if (kindSectionView.getVisibility() != View.VISIBLE
-                    || kindSectionView.isMarkedForRemoval()) {
-                continue;
-            }
-            if (!iconVisible) {
-                kindSectionView.setIconVisibility(true);
-                iconVisible = true;
-            } else {
-                kindSectionView.setIconVisibility(false);
-            }
-        }
-    }
-
-    private static boolean hasNonEmptyValuesDelta(RawContactDelta rawContactDelta,
-            String mimeType, DataKind dataKind) {
-        return !getNonEmptyValuesDeltas(rawContactDelta, mimeType, dataKind).isEmpty();
-    }
-
-    private static ValuesDelta getNonEmptySuperPrimaryValuesDeltas(RawContactDelta rawContactDelta,
-            String mimeType, DataKind dataKind) {
-        for (ValuesDelta valuesDelta : getNonEmptyValuesDeltas(
-                rawContactDelta, mimeType, dataKind)) {
-            if (valuesDelta.isSuperPrimary()) {
-                return valuesDelta;
-            }
-        }
-        return null;
-    }
-
-    static List<ValuesDelta> getNonEmptyValuesDeltas(RawContactDelta rawContactDelta,
-            String mimeType, DataKind dataKind) {
-        final List<ValuesDelta> result = new ArrayList<>();
-        if (rawContactDelta == null) {
-            vlog("Null RawContactDelta");
-            return result;
-        }
-        if (!rawContactDelta.hasMimeEntries(mimeType)) {
-            vlog("No ValueDeltas");
-            return result;
-        }
-        for (ValuesDelta valuesDelta : rawContactDelta.getMimeEntries(mimeType)) {
-            if (hasNonEmptyValue(dataKind, valuesDelta)) {
-                result.add(valuesDelta);
-            }
-        }
-        return result;
-    }
-
-    private static boolean hasNonEmptyValue(DataKind dataKind, ValuesDelta valuesDelta) {
-        if (valuesDelta == null) {
-            vlog("Null valuesDelta");
-            return false;
-        }
-        for (EditField editField : dataKind.fieldList) {
-            final String column = editField.column;
-            final String value = valuesDelta == null ? null : valuesDelta.getAsString(column);
-            vlog("Field " + column + " empty=" + TextUtils.isEmpty(value) + " value=" + value);
-            if (!TextUtils.isEmpty(value)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     private StructuredNameEditorView inflateStructuredNameEditorView(ViewGroup viewGroup,
             AccountType accountType, ValuesDelta valuesDelta, RawContactDelta rawContactDelta,
             NameEditorListener nameEditorListener, boolean readOnly) {
@@ -858,54 +751,45 @@
             result.setEditorListener(nameEditorListener);
         }
         result.setDeletable(false);
-        result.setValues(
-                accountType.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME),
-                valuesDelta,
-                rawContactDelta,
-                readOnly,
-                mViewIdGenerator);
+        result.setValues(accountType.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME),
+                valuesDelta, rawContactDelta, readOnly, mViewIdGenerator);
         return result;
     }
 
-    private PhoneticNameEditorView inflatePhoneticNameEditorView(ViewGroup viewGroup,
-            AccountType accountType, ValuesDelta valuesDelta, RawContactDelta rawContactDelta) {
-        final PhoneticNameEditorView result = (PhoneticNameEditorView) mLayoutInflater.inflate(
-                R.layout.phonetic_name_editor_view, viewGroup, /* attachToRoot =*/ false);
-        result.setDeletable(false);
-        result.setValues(
-                accountType.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME),
-                valuesDelta,
-                rawContactDelta,
-                /* readOnly =*/ false,
+    private CompactKindSectionView inflateKindSectionView(ViewGroup viewGroup,
+            List<KindSectionData> kindSectionDataList, String mimeType) {
+        final CompactKindSectionView kindSectionView = (CompactKindSectionView)
+                mLayoutInflater.inflate(R.layout.compact_item_kind_section, viewGroup,
+                        /* attachToRoot =*/ false);
+
+        if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)
+                || Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            // Phone numbers and emails address are always displayed and are
+            // the only types you add new values to initially
+            kindSectionView.setHideWhenEmpty(false);
+        }
+        // TODO: talk to Ricardo about when to allow adding new entries in compact mode
+        kindSectionView.setShowOneEmptyEditor(true);
+
+        kindSectionView.setState(kindSectionDataList, /* readOnly =*/ false,
                 mViewIdGenerator);
-        return result;
+
+        return kindSectionView;
     }
 
-    private TextFieldsEditorView inflateNicknameEditorView(ViewGroup viewGroup, DataKind dataKind,
-            ValuesDelta valuesDelta, RawContactDelta rawContactDelta) {
-        final TextFieldsEditorView result = (TextFieldsEditorView) mLayoutInflater.inflate(
-                R.layout.nick_name_editor_view, viewGroup, /* attachToRoot =*/ false);
-        result.setDeletable(false);
-        result.setValues(
-                dataKind,
-                valuesDelta,
-                rawContactDelta,
-                /* readOnly =*/ false,
-                mViewIdGenerator);
-        return result;
-    }
-
-
-    private KindSectionView inflateKindSectionView(ViewGroup viewGroup, DataKind dataKind,
-            RawContactDelta rawContactDelta) {
-        final KindSectionView result = (KindSectionView) mLayoutInflater.inflate(
-                R.layout.item_kind_section, viewGroup, /* attachToRoot =*/ false);
-        result.setState(
-                dataKind,
-                rawContactDelta,
-                /* readOnly =*/ false,
-                mViewIdGenerator);
-        return result;
+    private void updateMoreFieldsButton() {
+        // If any kind section views are hidden then show the link
+        for (int i = 0; i < mKindSectionViews.getChildCount(); i++) {
+            final CompactKindSectionView kindSectionView =
+                    (CompactKindSectionView) mKindSectionViews.getChildAt(i);
+            if (kindSectionView.getVisibility() == View.GONE) {
+                // Show the more fields button
+                mMoreFields.setVisibility(View.VISIBLE);
+                return;
+            }
+        }
+        // Hide the more fields button
+        mMoreFields.setVisibility(View.GONE);
     }
 
     private static void vlog(String message) {
diff --git a/src/com/android/contacts/editor/EditorUiUtils.java b/src/com/android/contacts/editor/EditorUiUtils.java
index 56be3c0..7a40b1d 100644
--- a/src/com/android/contacts/editor/EditorUiUtils.java
+++ b/src/com/android/contacts/editor/EditorUiUtils.java
@@ -16,12 +16,22 @@
 
 package com.android.contacts.editor;
 
-import static android.provider.ContactsContract.CommonDataKinds.Event;
 import static android.provider.ContactsContract.CommonDataKinds.GroupMembership;
-import static android.provider.ContactsContract.CommonDataKinds.Photo;
 import static android.provider.ContactsContract.CommonDataKinds.StructuredName;
 
 import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
 import android.text.TextUtils;
 import android.util.Pair;
 import com.android.contacts.R;
@@ -142,4 +152,39 @@
         }
         return builder.toString();
     }
+
+    /**
+     * Return an icon that represents {@param mimeType}.
+     */
+    public static Drawable getMimeTypeDrawable(Context context, String mimeType) {
+        switch (mimeType) {
+            case StructuredPostal.CONTENT_ITEM_TYPE:
+                return context.getResources().getDrawable(R.drawable.ic_place_24dp);
+            case SipAddress.CONTENT_ITEM_TYPE:
+                return context.getResources().getDrawable(R.drawable.ic_dialer_sip_black_24dp);
+            case Phone.CONTENT_ITEM_TYPE:
+                return context.getResources().getDrawable(R.drawable.ic_phone_24dp);
+            case Im.CONTENT_ITEM_TYPE:
+                return context.getResources().getDrawable(R.drawable.ic_message_24dp);
+            case Event.CONTENT_ITEM_TYPE:
+                return context.getResources().getDrawable(R.drawable.ic_event_24dp);
+            case Email.CONTENT_ITEM_TYPE:
+                return context.getResources().getDrawable(R.drawable.ic_email_24dp);
+            case Website.CONTENT_ITEM_TYPE:
+                return context.getResources().getDrawable(R.drawable.ic_public_black_24dp);
+            case Photo.CONTENT_ITEM_TYPE:
+                return context.getResources().getDrawable(R.drawable.ic_camera_alt_black_24dp);
+            case GroupMembership.CONTENT_ITEM_TYPE:
+                return context.getResources().getDrawable(R.drawable.ic_people_black_24dp);
+            case Organization.CONTENT_ITEM_TYPE:
+                return context.getResources().getDrawable(R.drawable.ic_business_black_24dp);
+            case Note.CONTENT_ITEM_TYPE:
+                return context.getResources().getDrawable(R.drawable.ic_insert_comment_black_24dp);
+            case Relation.CONTENT_ITEM_TYPE:
+                return context.getResources().getDrawable(
+                        R.drawable.ic_circles_extended_black_24dp);
+            default:
+                return null;
+        }
+    }
 }
diff --git a/src/com/android/contacts/editor/KindSectionView.java b/src/com/android/contacts/editor/KindSectionView.java
index 26ef058..a75c2e4 100644
--- a/src/com/android/contacts/editor/KindSectionView.java
+++ b/src/com/android/contacts/editor/KindSectionView.java
@@ -17,19 +17,6 @@
 package com.android.contacts.editor;
 
 import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.provider.Contacts.GroupMembership;
-import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.Event;
-import android.provider.ContactsContract.CommonDataKinds.Im;
-import android.provider.ContactsContract.CommonDataKinds.Note;
-import android.provider.ContactsContract.CommonDataKinds.Organization;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.Photo;
-import android.provider.ContactsContract.CommonDataKinds.Relation;
-import android.provider.ContactsContract.CommonDataKinds.SipAddress;
-import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
-import android.provider.ContactsContract.CommonDataKinds.Website;
 import android.provider.ContactsContract.Data;
 import android.text.TextUtils;
 import android.util.AttributeSet;
@@ -200,7 +187,7 @@
                 : getResources().getString(kind.titleRes);
         mIcon.setContentDescription(titleString);
 
-        mIcon.setImageDrawable(getMimeTypeDrawable(kind.mimeType));
+        mIcon.setImageDrawable(EditorUiUtils.getMimeTypeDrawable(getContext(), kind.mimeType));
         if (mIcon.getDrawable() == null) {
             mIcon.setContentDescription(null);
         }
@@ -355,38 +342,4 @@
     public DataKind getKind() {
         return mKind;
     }
-
-    /**
-     * Return an icon that represents {@param mimeType}.
-     */
-    private Drawable getMimeTypeDrawable(String mimeType) {
-        switch (mimeType) {
-            case StructuredPostal.CONTENT_ITEM_TYPE:
-                return getResources().getDrawable(R.drawable.ic_place_24dp);
-            case SipAddress.CONTENT_ITEM_TYPE:
-                return getResources().getDrawable(R.drawable.ic_dialer_sip_black_24dp);
-            case Phone.CONTENT_ITEM_TYPE:
-                return getResources().getDrawable(R.drawable.ic_phone_24dp);
-            case Im.CONTENT_ITEM_TYPE:
-                return getResources().getDrawable(R.drawable.ic_message_24dp);
-            case Event.CONTENT_ITEM_TYPE:
-                return getResources().getDrawable(R.drawable.ic_event_24dp);
-            case Email.CONTENT_ITEM_TYPE:
-                return getResources().getDrawable(R.drawable.ic_email_24dp);
-            case Website.CONTENT_ITEM_TYPE:
-                return getResources().getDrawable(R.drawable.ic_public_black_24dp);
-            case Photo.CONTENT_ITEM_TYPE:
-                return getResources().getDrawable(R.drawable.ic_camera_alt_black_24dp);
-            case GroupMembership.CONTENT_ITEM_TYPE:
-                return getResources().getDrawable(R.drawable.ic_people_black_24dp);
-            case Organization.CONTENT_ITEM_TYPE:
-                return getResources().getDrawable(R.drawable.ic_business_black_24dp);
-            case Note.CONTENT_ITEM_TYPE:
-                return getResources().getDrawable(R.drawable.ic_insert_comment_black_24dp);
-            case Relation.CONTENT_ITEM_TYPE:
-                return getResources().getDrawable(R.drawable.ic_circles_extended_black_24dp);
-            default:
-                return null;
-        }
-    }
 }
diff --git a/src/com/android/contacts/editor/TextFieldsEditorView.java b/src/com/android/contacts/editor/TextFieldsEditorView.java
index fe476ed..51c9d94 100644
--- a/src/com/android/contacts/editor/TextFieldsEditorView.java
+++ b/src/com/android/contacts/editor/TextFieldsEditorView.java
@@ -213,7 +213,7 @@
         }
         boolean hidePossible = false;
 
-        int fieldCount = kind.fieldList.size();
+        int fieldCount = kind.fieldList == null ? 0 : kind.fieldList.size();
         mFieldEditTexts = new EditText[fieldCount];
         for (int index = 0; index < fieldCount; index++) {
             final EditField field = kind.fieldList.get(index);