Merge "Fix bugs on Contact editor." into ub-contactsdialer-a-dev
diff --git a/src/com/android/contacts/editor/CompactContactEditorFragment.java b/src/com/android/contacts/editor/CompactContactEditorFragment.java
index d073f68..f081d54 100644
--- a/src/com/android/contacts/editor/CompactContactEditorFragment.java
+++ b/src/com/android/contacts/editor/CompactContactEditorFragment.java
@@ -21,17 +21,12 @@
 import com.android.contacts.activities.CompactContactEditorActivity;
 import com.android.contacts.activities.ContactEditorActivity;
 import com.android.contacts.activities.ContactEditorBaseActivity;
-import com.android.contacts.common.model.AccountTypeManager;
 import com.android.contacts.common.model.RawContactDelta;
-import com.android.contacts.common.model.RawContactDeltaList;
 import com.android.contacts.common.model.ValuesDelta;
-import com.android.contacts.common.model.account.AccountType;
 import com.android.contacts.common.model.account.AccountWithDataSet;
-import com.android.contacts.detail.PhotoSelectionHandler;
 import com.android.contacts.util.ContactPhotoUtils;
 
 import android.app.Activity;
-import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.net.Uri;
@@ -114,7 +109,10 @@
         final CompactRawContactsEditorView editorView = getContent();
         editorView.setListener(this);
         editorView.setState(mState, getMaterialPalette(), mViewIdGenerator, mPhotoId,
-                mHasNewContact, mIsUserProfile, mAccountWithDataSet);
+                mReadOnlyDisplayName, mHasNewContact, mIsUserProfile, mAccountWithDataSet);
+        if (mReadOnlyDisplayName != null) {
+            mReadOnlyNameEditorView = editorView.getPrimaryNameEditorView();
+        }
 
         // Set up the photo widget
         editorView.setPhotoListener(this);
diff --git a/src/com/android/contacts/editor/CompactKindSectionView.java b/src/com/android/contacts/editor/CompactKindSectionView.java
index 448e7c5..3ebe27b 100644
--- a/src/com/android/contacts/editor/CompactKindSectionView.java
+++ b/src/com/android/contacts/editor/CompactKindSectionView.java
@@ -18,7 +18,9 @@
 
 import android.content.Context;
 import android.database.Cursor;
+import android.provider.ContactsContract.CommonDataKinds.Event;
 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
@@ -30,6 +32,7 @@
 
 import com.android.contacts.R;
 import com.android.contacts.common.model.RawContactDelta;
+import com.android.contacts.common.model.RawContactDeltaList;
 import com.android.contacts.common.model.RawContactModifier;
 import com.android.contacts.common.model.ValuesDelta;
 import com.android.contacts.common.model.account.AccountType;
@@ -145,7 +148,7 @@
         }
     }
 
-    private List<KindSectionData> mKindSectionDataList;
+    private KindSectionDataList mKindSectionDataList;
     private ViewIdGenerator mViewIdGenerator;
     private CompactRawContactsEditorView.Listener mListener;
 
@@ -216,24 +219,78 @@
     }
 
     /**
+     * Whether this is a name kind section view and all name fields (structured, phonetic,
+     * and nicknames) are empty.
+     */
+    public boolean isEmptyName() {
+        if (!StructuredName.CONTENT_ITEM_TYPE.equals(mKindSectionDataList.getMimeType())) {
+            return false;
+        }
+        for (int i = 0; i < mEditors.getChildCount(); i++) {
+            final View view = mEditors.getChildAt(i);
+            if (view instanceof Editor) {
+                final Editor editor = (Editor) view;
+                if (!editor.isEmpty()) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Sets the given display name as the structured name as if the user input it, but
+     * without informing editor listeners.
+     */
+    public void setName(String displayName) {
+        if (!StructuredName.CONTENT_ITEM_TYPE.equals(mKindSectionDataList.getMimeType())) {
+            return;
+        }
+        for (int i = 0; i < mEditors.getChildCount(); i++) {
+            final View view = mEditors.getChildAt(i);
+            if (view instanceof StructuredNameEditorView) {
+                final StructuredNameEditorView editor = (StructuredNameEditorView) view;
+
+                // Detach listeners since so we don't show suggested aggregations
+                final Editor.EditorListener editorListener = editor.getEditorListener();
+                editor.setEditorListener(null);
+
+                editor.setDisplayName(displayName);
+
+                // Reattach listeners
+                editor.setEditorListener(editorListener);
+
+                return;
+            }
+        }
+    }
+
+    public StructuredNameEditorView getPrimaryNameEditorView() {
+        if (!StructuredName.CONTENT_ITEM_TYPE.equals(mKindSectionDataList.getMimeType())
+            || mEditors.getChildCount() == 0) {
+            return null;
+        }
+        return (StructuredNameEditorView) mEditors.getChildAt(0);
+    }
+
+    /**
      * Binds views for the given {@link KindSectionData} list.
      *
      * We create a structured name and phonetic name editor for each {@link DataKind} with a
-     * {@link }StructuredName#CONTENT_ITEM_TYPE} mime type.  The number and order of editors are
+     * {@link StructuredName#CONTENT_ITEM_TYPE} mime type.  The number and order of editors are
      * rendered as they are given to {@link #setState}.
      *
      * Empty name editors are never added and at least one structured name editor is always
      * displayed, even if it is empty.
      */
-    public void setState(List<KindSectionData> kindSectionDataList,
+    public void setState(KindSectionDataList kindSectionDataList,
             ViewIdGenerator viewIdGenerator, CompactRawContactsEditorView.Listener listener) {
         mKindSectionDataList = kindSectionDataList;
         mViewIdGenerator = viewIdGenerator;
         mListener = listener;
 
         // Set the icon using the first DataKind
-        final DataKind dataKind = mKindSectionDataList.isEmpty()
-                ? null : mKindSectionDataList.get(0).getDataKind();
+        final DataKind dataKind = mKindSectionDataList.getDataKind();
         if (dataKind != null) {
             mIcon.setImageDrawable(EditorUiUtils.getMimeTypeDrawable(getContext(),
                     dataKind.mimeType));
@@ -263,9 +320,9 @@
                         kindSectionData.getDataKind());
             } else {
                 final Editor.EditorListener editorListener;
-                if (kindSectionData.isNicknameDataKind()) {
+                if (Nickname.CONTENT_ITEM_TYPE.equals(kindSectionData.getDataKind().mimeType)) {
                     editorListener = new OtherNameKindEditorListener();
-                } else if (kindSectionData.isEventDataKind()) {
+                } else if (Event.CONTENT_ITEM_TYPE.equals(kindSectionData.getDataKind().mimeType)) {
                     editorListener = new EventEditorListener();
                 } else {
                     editorListener = new NonNameEditorListener();
@@ -379,14 +436,14 @@
      * then the entire section is hidden.
      */
     public void updateEmptyEditors(boolean shouldAnimate) {
-        final boolean isNameKindSection = mKindSectionDataList.get(0).isNameDataKind();
+        final boolean isNameKindSection = StructuredName.CONTENT_ITEM_TYPE.equals(
+                mKindSectionDataList.getMimeType());
         final boolean isGroupKindSection = GroupMembership.CONTENT_ITEM_TYPE.equals(
-                mKindSectionDataList.get(0).getDataKind().mimeType);
+                mKindSectionDataList.getMimeType());
 
         if (isNameKindSection) {
             // The name kind section is always visible
             setVisibility(VISIBLE);
-
             updateEmptyNameEditors(shouldAnimate);
         } else if (isGroupKindSection) {
             // Check whether metadata has been bound for all group views
@@ -425,29 +482,37 @@
 
         for (int i = 0; i < mEditors.getChildCount(); i++) {
             final View view = mEditors.getChildAt(i);
-            if (!(view instanceof Editor)) continue; // Skip read-only names
-            final Editor editor = (Editor) view;
-            if (view instanceof StructuredNameEditorView) {
-                // We always show one empty structured name view
-                if (editor.isEmpty()) {
-                    if (isEmptyNameEditorVisible) {
-                        // If we're already showing an empty editor then hide any other empties
-                        if (mHideIfEmpty) {
-                            view.setVisibility(View.GONE);
+            if (view instanceof Editor) {
+                final Editor editor = (Editor) view;
+                if (view instanceof StructuredNameEditorView) {
+                    // We always show one empty structured name view
+                    if (editor.isEmpty()) {
+                        if (isEmptyNameEditorVisible) {
+                            // If we're already showing an empty editor then hide any other empties
+                            if (mHideIfEmpty) {
+                                view.setVisibility(View.GONE);
+                            }
+                        } else {
+                            isEmptyNameEditorVisible = true;
                         }
                     } else {
+                        showView(view, shouldAnimate);
                         isEmptyNameEditorVisible = true;
                     }
                 } else {
-                    showView(view, shouldAnimate);
-                    isEmptyNameEditorVisible = true;
+                    // Since we can't add phonetic names and nicknames, just show or hide them
+                    if (mHideIfEmpty && editor.isEmpty()) {
+                        hideView(view);
+                    } else {
+                        showView(view, /* shouldAnimate =*/ false); // Animation here causes jank
+                    }
                 }
             } else {
-                // For phonetic names and nicknames, which can't be added, just show or hide them
-                if (mHideIfEmpty && editor.isEmpty()) {
+                // For read only names, only show them if we're not hiding empty views
+                if (mHideIfEmpty) {
                     hideView(view);
                 } else {
-                    showView(view, /* shouldAnimate =*/ false); // Animation here causes jank
+                    showView(view, shouldAnimate);
                 }
             }
         }
@@ -488,8 +553,9 @@
             final RawContactDelta rawContactDelta =
                     mKindSectionDataList.get(0).getRawContactDelta();
             final ValuesDelta values = RawContactModifier.insertChild(rawContactDelta, dataKind);
-            final Editor.EditorListener editorListener = mKindSectionDataList.get(0)
-                    .isEventDataKind() ? new EventEditorListener() : new NonNameEditorListener();
+            final String mimeType = mKindSectionDataList.getMimeType();
+            final Editor.EditorListener editorListener = Event.CONTENT_ITEM_TYPE.equals(mimeType)
+                    ? new EventEditorListener() : new NonNameEditorListener();
             final View view = addNonNameEditorView(rawContactDelta, dataKind, values,
                     editorListener);
             showView(view, shouldAnimate);
diff --git a/src/com/android/contacts/editor/CompactRawContactsEditorView.java b/src/com/android/contacts/editor/CompactRawContactsEditorView.java
index a6f2d1c..ed5dc3b 100644
--- a/src/com/android/contacts/editor/CompactRawContactsEditorView.java
+++ b/src/com/android/contacts/editor/CompactRawContactsEditorView.java
@@ -17,7 +17,6 @@
 package com.android.contacts.editor;
 
 import com.android.contacts.R;
-import com.android.contacts.common.ContactsUtils;
 import com.android.contacts.common.model.AccountTypeManager;
 import com.android.contacts.common.model.RawContactDelta;
 import com.android.contacts.common.model.RawContactDeltaList;
@@ -28,7 +27,6 @@
 import com.android.contacts.common.model.dataitem.DataKind;
 import com.android.contacts.common.util.AccountsListAdapter;
 import com.android.contacts.common.util.MaterialColorMapUtils;
-import com.android.contacts.util.ContactPhotoUtils;
 import com.android.contacts.util.UiClosables;
 
 import android.content.ContentUris;
@@ -69,7 +67,6 @@
 import android.widget.ListPopupWindow;
 import android.widget.TextView;
 
-import java.io.File;
 import java.io.FileNotFoundException;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -79,7 +76,6 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Set;
 import java.util.TreeSet;
 
@@ -88,7 +84,7 @@
  */
 public class CompactRawContactsEditorView extends LinearLayout implements View.OnClickListener {
 
-    private static final String TAG = "CompactEditorView";
+    static final String TAG = "CompactEditorView";
 
     private static final KindSectionDataMapEntryComparator
             KIND_SECTION_DATA_MAP_ENTRY_COMPARATOR = new KindSectionDataMapEntryComparator();
@@ -193,13 +189,13 @@
 
     /** Used to sort entire kind sections. */
     private static final class KindSectionDataMapEntryComparator implements
-            Comparator<Map.Entry<String,List<KindSectionData>>> {
+            Comparator<Map.Entry<String,KindSectionDataList>> {
 
         final MimeTypeComparator mMimeTypeComparator = new MimeTypeComparator();
 
         @Override
-        public int compare(Map.Entry<String, List<KindSectionData>> entry1,
-                Map.Entry<String, List<KindSectionData>> entry2) {
+        public int compare(Map.Entry<String, KindSectionDataList> entry1,
+                Map.Entry<String, KindSectionDataList> entry2) {
             if (entry1 == entry2) return 0;
             if (entry1 == null) return -1;
             if (entry2 == null) return 1;
@@ -291,9 +287,9 @@
      */
     private static final class NameEditorComparator implements Comparator<KindSectionData> {
 
-        private RawContactDeltaComparator mRawContactDeltaComparator;
-        private MimeTypeComparator mMimeTypeComparator;
-        private RawContactDelta mPrimaryRawContactDelta;
+        private final RawContactDeltaComparator mRawContactDeltaComparator;
+        private final MimeTypeComparator mMimeTypeComparator;
+        private final RawContactDelta mPrimaryRawContactDelta;
 
         private NameEditorComparator(Context context, RawContactDelta primaryRawContactDelta) {
             mRawContactDeltaComparator = new RawContactDeltaComparator(context);
@@ -372,11 +368,12 @@
     private ViewIdGenerator mViewIdGenerator;
     private MaterialColorMapUtils.MaterialPalette mMaterialPalette;
     private long mPhotoId;
+    private String mReadOnlyDisplayName;
     private boolean mHasNewContact;
     private boolean mIsUserProfile;
     private AccountWithDataSet mPrimaryAccount;
     private RawContactDelta mPrimaryRawContactDelta;
-    private Map<String,List<KindSectionData>> mKindSectionDataMap = new HashMap<>();
+    private Map<String,KindSectionDataList> mKindSectionDataMap = new HashMap<>();
 
     // Account header
     private View mAccountHeaderContainer;
@@ -401,8 +398,10 @@
     private View mMoreFields;
 
     private boolean mIsExpanded;
+
     private long mPhotoRawContactId;
     private ValuesDelta mPhotoValuesDelta;
+    private StructuredNameEditorView mPrimaryNameEditorView;
 
     public CompactRawContactsEditorView(Context context) {
         super(context);
@@ -545,6 +544,10 @@
         return mPhotoRawContactId;
     }
 
+    public StructuredNameEditorView getPrimaryNameEditorView() {
+        return mPrimaryNameEditorView;
+    }
+
     /**
      * Returns a data holder for every non-default/non-empty photo from each raw contact, whether
      * the raw contact is writable or not.
@@ -639,8 +642,8 @@
 
     public void setState(RawContactDeltaList rawContactDeltas,
             MaterialColorMapUtils.MaterialPalette materialPalette, ViewIdGenerator viewIdGenerator,
-            long photoId, boolean hasNewContact, boolean isUserProfile,
-            AccountWithDataSet primaryAccount) {
+            long photoId, String readOnlyDisplayName, boolean hasNewContact,
+            boolean isUserProfile, AccountWithDataSet primaryAccount) {
         mKindSectionDataMap.clear();
         mKindSectionViews.removeAllViews();
         mMoreFields.setVisibility(View.VISIBLE);
@@ -648,6 +651,7 @@
         mMaterialPalette = materialPalette;
         mViewIdGenerator = viewIdGenerator;
         mPhotoId = photoId;
+        mReadOnlyDisplayName = readOnlyDisplayName;
         mHasNewContact = hasNewContact;
         mIsUserProfile = isUserProfile;
         mPrimaryAccount = primaryAccount;
@@ -662,53 +666,14 @@
             if (mListener != null) mListener.onBindEditorsFailed();
             return;
         }
-        parseRawContactDeltas(rawContactDeltas, mPrimaryAccount);
+        parseRawContactDeltas(rawContactDeltas);
         if (mKindSectionDataMap.isEmpty()) {
             elog("No kind section data parsed from RawContactDelta(s)");
             if (mListener != null) mListener.onBindEditorsFailed();
             return;
         }
-
-        // Setup the view
-        addAccountInfo(rawContactDeltas);
-        addPhotoView();
-        addKindSectionViews();
-
-        if (mIsExpanded) {
-            showAllFields();
-        }
-
-        if (mListener != null) mListener.onEditorsBound();
-    }
-
-    private void parseRawContactDeltas(RawContactDeltaList rawContactDeltas,
-            AccountWithDataSet primaryAccount) {
-        if (primaryAccount != null) {
-            // Use the first writable contact that matches the primary account
-            for (RawContactDelta rawContactDelta : rawContactDeltas) {
-                if (!rawContactDelta.isVisible()) continue;
-                final AccountType accountType = rawContactDelta.getAccountType(mAccountTypeManager);
-                if (accountType == null || !accountType.areContactsWritable()) continue;
-                if (matchesAccount(primaryAccount, rawContactDelta)) {
-                    vlog("parse: matched primary account raw contact");
-                    mPrimaryRawContactDelta = rawContactDelta;
-                    break;
-                }
-            }
-        }
-        if (mPrimaryRawContactDelta == null) {
-            // Fall back to the first writable raw contact
-            for (RawContactDelta rawContactDelta : rawContactDeltas) {
-                if (!rawContactDelta.isVisible()) continue;
-                final AccountType accountType = rawContactDelta.getAccountType(mAccountTypeManager);
-                if (accountType != null && accountType.areContactsWritable()) {
-                    vlog("parse: falling back to the first writable raw contact as primary");
-                    mPrimaryRawContactDelta = rawContactDelta;
-                    break;
-                }
-            }
-        }
-
+        mPrimaryRawContactDelta = mKindSectionDataMap.get(StructuredName.CONTENT_ITEM_TYPE)
+                .getEntryToWrite(mPrimaryAccount, mHasNewContact).first.getRawContactDelta();
         if (mPrimaryRawContactDelta != null) {
             RawContactModifier.ensureKindExists(mPrimaryRawContactDelta,
                     mPrimaryRawContactDelta.getAccountType(mAccountTypeManager),
@@ -718,6 +683,19 @@
                     Photo.CONTENT_ITEM_TYPE);
         }
 
+        // Setup the view
+        addAccountInfo(rawContactDeltas);
+        addPhotoView();
+        addKindSectionViews();
+        if (mHasNewContact) {
+            maybeCopyPrimaryDisplayName();
+        }
+        if (mIsExpanded) showAllFields();
+
+        if (mListener != null) mListener.onEditorsBound();
+    }
+
+    private void parseRawContactDeltas(RawContactDeltaList rawContactDeltas) {
         // Build the kind section data list map
         vlog("parse: " + rawContactDeltas.size() + " rawContactDelta(s)");
         for (int j = 0; j < rawContactDeltas.size(); j++) {
@@ -745,7 +723,7 @@
                 }
 
                 final List<KindSectionData> kindSectionDataList =
-                        getKindSectionDataList(mimeType);
+                        getOrCreateKindSectionDataList(mimeType);
                 final KindSectionData kindSectionData =
                         new KindSectionData(accountType, dataKind, rawContactDelta);
                 kindSectionDataList.add(kindSectionData);
@@ -762,27 +740,18 @@
         }
     }
 
-    private List<KindSectionData> getKindSectionDataList(String mimeType) {
+    private List<KindSectionData> getOrCreateKindSectionDataList(String mimeType) {
         // Put structured names and nicknames together
         mimeType = Nickname.CONTENT_ITEM_TYPE.equals(mimeType)
                 ? StructuredName.CONTENT_ITEM_TYPE : mimeType;
-        List<KindSectionData> kindSectionDataList = mKindSectionDataMap.get(mimeType);
+        KindSectionDataList kindSectionDataList = mKindSectionDataMap.get(mimeType);
         if (kindSectionDataList == null) {
-            kindSectionDataList = new ArrayList<>();
+            kindSectionDataList = new KindSectionDataList();
             mKindSectionDataMap.put(mimeType, kindSectionDataList);
         }
         return kindSectionDataList;
     }
 
-    /** Whether the given RawContactDelta belong to the given account. */
-    private boolean matchesAccount(AccountWithDataSet accountWithDataSet,
-            RawContactDelta rawContactDelta) {
-        if (accountWithDataSet == null) return false;
-        return Objects.equals(accountWithDataSet.name, rawContactDelta.getAccountName())
-                && Objects.equals(accountWithDataSet.type, rawContactDelta.getAccountType())
-                && Objects.equals(accountWithDataSet.dataSet, rawContactDelta.getDataSet());
-    }
-
     private void addAccountInfo(RawContactDeltaList rawContactDeltas) {
         if (mPrimaryRawContactDelta == null) {
             mAccountHeaderContainer.setVisibility(View.GONE);
@@ -967,123 +936,44 @@
 
     private void addPhotoView() {
         // Get the kind section data and values delta that we will display in the photo view
-        Pair<KindSectionData,ValuesDelta> pair = getPrimaryPhotoKindSectionData(mPhotoId);
-        if (pair == null) {
+        final KindSectionDataList kindSectionDataList =
+                mKindSectionDataMap.get(Photo.CONTENT_ITEM_TYPE);
+        final Pair<KindSectionData,ValuesDelta> photoToDisplay =
+                kindSectionDataList.getEntryToDisplay(mPhotoId);
+        if (photoToDisplay == null) {
             wlog("photo: no kind section data parsed");
-            mPhotoView.setReadOnly(true);
+            mPhotoView.setVisibility(View.GONE);
             return;
         }
 
         // Set the photo view
-        final ValuesDelta primaryValuesDelta = pair.second;
-        mPhotoView.setPhoto(primaryValuesDelta, mMaterialPalette);
+        mPhotoView.setPhoto(photoToDisplay.second, mMaterialPalette);
 
         // Find the raw contact ID and values delta that will be written when the photo is edited
-        final KindSectionData primaryKindSectionData = pair.first;
-        if (mHasNewContact && mPrimaryRawContactDelta != null
-                && !primaryKindSectionData.getValuesDeltas().isEmpty()) {
-            // If we're editing a read-only contact we want to display the photo from the
-            // read-only contact in a photo editor view, but update the new raw contact
-            // that was created.
-            mPhotoRawContactId = mPrimaryRawContactDelta.getRawContactId();
-            mPhotoValuesDelta = primaryKindSectionData.getValuesDeltas().get(0);
-            mPhotoView.setReadOnly(false);
-            return;
-        }
-        if (primaryKindSectionData.getAccountType().areContactsWritable() &&
-                !primaryKindSectionData.getValuesDeltas().isEmpty()) {
-            mPhotoRawContactId = primaryKindSectionData.getRawContactDelta().getRawContactId();
-            mPhotoValuesDelta = primaryKindSectionData.getValuesDeltas().get(0);
-            mPhotoView.setReadOnly(false);
-            return;
-        }
-
-        final KindSectionData writableKindSectionData = getFirstWritablePhotoKindSectionData();
-        if (writableKindSectionData == null
-                || writableKindSectionData.getValuesDeltas().isEmpty()) {
+        final Pair<KindSectionData,ValuesDelta> photoToWrite = kindSectionDataList.getEntryToWrite(
+                mPrimaryAccount, mHasNewContact);
+        if (photoToWrite == null) {
             mPhotoView.setReadOnly(true);
             return;
         }
-        mPhotoRawContactId = writableKindSectionData.getRawContactDelta().getRawContactId();
-        mPhotoValuesDelta = writableKindSectionData.getValuesDeltas().get(0);
         mPhotoView.setReadOnly(false);
-    }
-
-    private Pair<KindSectionData,ValuesDelta> getPrimaryPhotoKindSectionData(long id) {
-        final String mimeType = Photo.CONTENT_ITEM_TYPE;
-        final List<KindSectionData> kindSectionDataList = mKindSectionDataMap.get(mimeType);
-
-        KindSectionData resultKindSectionData = null;
-        ValuesDelta resultValuesDelta = null;
-        if (id > 0) {
-            // Look for a match for the ID that was passed in
-            for (KindSectionData kindSectionData : kindSectionDataList) {
-                resultValuesDelta = kindSectionData.getValuesDeltaById(id);
-                if (resultValuesDelta != null) {
-                    vlog("photo: matched kind section data by ID");
-                    resultKindSectionData = kindSectionData;
-                    break;
-                }
-            }
-        }
-        if (resultKindSectionData == null) {
-            // Look for a super primary photo
-            for (KindSectionData kindSectionData : kindSectionDataList) {
-                resultValuesDelta = kindSectionData.getSuperPrimaryValuesDelta();
-                if (resultValuesDelta != null) {
-                    wlog("photo: matched super primary kind section data");
-                    resultKindSectionData = kindSectionData;
-                    break;
-                }
-            }
-        }
-        if (resultKindSectionData == null) {
-            // Fall back to the first non-empty value
-            for (KindSectionData kindSectionData : kindSectionDataList) {
-                resultValuesDelta = kindSectionData.getFirstNonEmptyValuesDelta();
-                if (resultValuesDelta != null) {
-                    vlog("photo: using first non empty value");
-                    resultKindSectionData = kindSectionData;
-                    break;
-                }
-            }
-        }
-        if (resultKindSectionData == null || resultValuesDelta == null) {
-            final List<ValuesDelta> valuesDeltaList = kindSectionDataList.get(0).getValuesDeltas();
-            if (valuesDeltaList != null && !valuesDeltaList.isEmpty()) {
-                vlog("photo: falling back to first empty entry");
-                resultValuesDelta = valuesDeltaList.get(0);
-                resultKindSectionData = kindSectionDataList.get(0);
-            }
-        }
-        return resultKindSectionData != null && resultValuesDelta != null
-                ? new Pair<>(resultKindSectionData, resultValuesDelta) : null;
-    }
-
-    private KindSectionData getFirstWritablePhotoKindSectionData() {
-        final String mimeType = Photo.CONTENT_ITEM_TYPE;
-        final List<KindSectionData> kindSectionDataList = mKindSectionDataMap.get(mimeType);
-        for (KindSectionData kindSectionData : kindSectionDataList) {
-            if (kindSectionData.getAccountType().areContactsWritable()) {
-                return kindSectionData;
-            }
-        }
-        return null;
+        mPhotoRawContactId = photoToWrite.first.getRawContactDelta().getRawContactId();
+        mPhotoValuesDelta = photoToWrite.second;
     }
 
     private void addKindSectionViews() {
         // Sort the kinds
-        final TreeSet<Map.Entry<String,List<KindSectionData>>> entries =
+        final TreeSet<Map.Entry<String,KindSectionDataList>> entries =
                 new TreeSet<>(KIND_SECTION_DATA_MAP_ENTRY_COMPARATOR);
         entries.addAll(mKindSectionDataMap.entrySet());
 
         vlog("kind: " + entries.size() + " kindSection(s)");
         int i = -1;
-        for (Map.Entry<String, List<KindSectionData>> entry : entries) {
+        for (Map.Entry<String, KindSectionDataList> entry : entries) {
             i++;
 
             final String mimeType = entry.getKey();
-            final List<KindSectionData> kindSectionDataList = entry.getValue();
+            final KindSectionDataList kindSectionDataList = entry.getValue();
 
             // Ignore mime types that we've already handled
             if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
@@ -1115,7 +1005,7 @@
     }
 
     private CompactKindSectionView inflateKindSectionView(ViewGroup viewGroup,
-            List<KindSectionData> kindSectionDataList, String mimeType) {
+            KindSectionDataList kindSectionDataList, String mimeType) {
         final CompactKindSectionView kindSectionView = (CompactKindSectionView)
                 mLayoutInflater.inflate(R.layout.compact_item_kind_section, viewGroup,
                         /* attachToRoot =*/ false);
@@ -1144,6 +1034,19 @@
         return kindSectionView;
     }
 
+    private void maybeCopyPrimaryDisplayName() {
+        if (TextUtils.isEmpty(mReadOnlyDisplayName)) return;
+        final List<CompactKindSectionView> kindSectionViews
+                = mKindSectionViewsMap.get(StructuredName.CONTENT_ITEM_TYPE);
+        if (kindSectionViews.isEmpty()) return;
+        final CompactKindSectionView primaryNameKindSectionView = kindSectionViews.get(0);
+        if (primaryNameKindSectionView.isEmptyName()) {
+            vlog("name: using read only display name as primary name");
+            primaryNameKindSectionView.setName(mReadOnlyDisplayName);
+            mPrimaryNameEditorView = primaryNameKindSectionView.getPrimaryNameEditorView();
+        }
+    }
+
     private void showAllFields() {
         // Stop hiding empty editors and allow the user to enter values for all kinds now
         for (int i = 0; i < mKindSectionViews.getChildCount(); i++) {
diff --git a/src/com/android/contacts/editor/ContactEditorBaseFragment.java b/src/com/android/contacts/editor/ContactEditorBaseFragment.java
index 02ac40b..54c3c07 100644
--- a/src/com/android/contacts/editor/ContactEditorBaseFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorBaseFragment.java
@@ -373,6 +373,17 @@
     // Join Activity
     protected long mContactIdForJoin;
 
+    //
+    // Not saved/restored on rotates
+    //
+
+    // Used to pre-populate the editor with a display name when a user edits a read-only contact.
+    protected String mReadOnlyDisplayName;
+
+    // The name editor view for the new raw contact that was created so that the user can
+    // edit a read-only contact (to which the new raw contact was joined)
+    protected StructuredNameEditorView mReadOnlyNameEditorView;
+
     /**
      * The contact data loader listener.
      */
@@ -963,12 +974,42 @@
      * Return true if there are any edits to the current contact which need to
      * be saved.
      */
-    protected boolean hasPendingChanges() {
+    protected boolean hasPendingRawContactChanges() {
         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
         return RawContactModifier.hasChanges(mState, accountTypes);
     }
 
     /**
+     * Determines if changes were made in the editor that need to be saved, while taking into
+     * account that name changes are not real for read-only contacts.
+     * See go/editing-read-only-contacts
+     */
+    protected boolean hasPendingChanges() {
+        if (mReadOnlyNameEditorView == null || mReadOnlyDisplayName == null) {
+            return hasPendingRawContactChanges();
+        }
+        // We created a new raw contact delta with a default display name.
+        // We must test for pending changes while ignoring the default display name.
+        final String displayName = mReadOnlyNameEditorView.getDisplayName();
+        if (mReadOnlyDisplayName.equals(displayName)) {
+            // The user did not modify the default display name, erase it and
+            // check if the user made any other changes
+            mReadOnlyNameEditorView.clearAllFields();
+            if (hasPendingRawContactChanges()) {
+                // Other changes were made to the aggregate contact, restore
+                // the display name and proceed.
+                mReadOnlyNameEditorView.setDisplayName(displayName);
+                return true;
+            } else {
+                // No other changes were made to the aggregate contact. Don't add back
+                // the displayName so that a "bogus" contact is not created.
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
      * Whether editor inputs and the options menu should be enabled.
      */
     protected boolean isEnabled() {
@@ -1078,6 +1119,7 @@
             }
         }
 
+        String readOnlyDisplayName = null;
         // Check for writable raw contacts.  If there are none, then we need to create one so user
         // can edit.  For the user profile case, there is already an editable contact.
         if (!contact.isUserProfile() && !contact.isWritableContact(mContext)) {
@@ -1085,11 +1127,13 @@
 
             // This is potentially an asynchronous call and will add deltas to list.
             selectAccountAndCreateContact();
+
+            readOnlyDisplayName = contact.getDisplayName();
         }
 
         // This also adds deltas to list.  If readOnlyDisplayName is null at this point it is
         // simply ignored later on by the editor.
-        setStateForExistingContact(contact.isUserProfile(), mRawContacts);
+        setStateForExistingContact(readOnlyDisplayName, contact.isUserProfile(), mRawContacts);
     }
 
     /**
@@ -1160,9 +1204,10 @@
     /**
      * Prepare {@link #mState} for an existing contact.
      */
-    protected void setStateForExistingContact(boolean isUserProfile,
+    protected void setStateForExistingContact(String readOnlyDisplayName, boolean isUserProfile,
             ImmutableList<RawContact> rawContacts) {
         setEnabled(true);
+        mReadOnlyDisplayName = readOnlyDisplayName;
 
         mState.addAll(rawContacts.iterator());
         setIntentExtras(mIntentExtras);
@@ -1274,7 +1319,8 @@
             setStateForNewContact(newAccount, newAccountType, oldState, oldAccountType,
                     isEditingUserProfile());
             if (mIsEdit) {
-                setStateForExistingContact(isEditingUserProfile(), mRawContacts);
+                setStateForExistingContact(mReadOnlyDisplayName, isEditingUserProfile(),
+                        mRawContacts);
             }
         }
     }
diff --git a/src/com/android/contacts/editor/KindSectionData.java b/src/com/android/contacts/editor/KindSectionData.java
index 6c33601..8921099 100644
--- a/src/com/android/contacts/editor/KindSectionData.java
+++ b/src/com/android/contacts/editor/KindSectionData.java
@@ -97,18 +97,6 @@
         return mDataKind;
     }
 
-    public boolean isNameDataKind() {
-        return StructuredName.CONTENT_ITEM_TYPE.equals(mDataKind.mimeType);
-    }
-
-    public boolean isNicknameDataKind() {
-        return Nickname.CONTENT_ITEM_TYPE.equals(mDataKind.mimeType);
-    }
-
-    public boolean isEventDataKind() {
-        return Event.CONTENT_ITEM_TYPE.equals(mDataKind.mimeType);
-    }
-
     public RawContactDelta getRawContactDelta() {
         return mRawContactDelta;
     }
diff --git a/src/com/android/contacts/editor/KindSectionDataList.java b/src/com/android/contacts/editor/KindSectionDataList.java
new file mode 100644
index 0000000..3658f21
--- /dev/null
+++ b/src/com/android/contacts/editor/KindSectionDataList.java
@@ -0,0 +1,188 @@
+/*
+ * 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 com.android.contacts.common.model.RawContactDelta;
+import com.android.contacts.common.model.ValuesDelta;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.model.dataitem.DataKind;
+
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.util.Log;
+import android.util.Pair;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Container for multiple {@link KindSectionData} objects.  Provides convenience methods for
+ * interrogating the collection for a certain KindSectionData item (e.g. the first writable, or
+ * "primary", one.  Also enforces that only items with the same DataKind/mime-type are added.
+ */
+public class KindSectionDataList extends ArrayList<KindSectionData> {
+
+    private static final String TAG = CompactRawContactsEditorView.TAG;
+
+    /**
+     * Returns the mime type for all DataKinds in this List.
+     */
+    public String getMimeType() {
+        if (isEmpty()) return null;
+        final String mimeType = get(0).getDataKind().mimeType;
+        // StructuredNames and Nicknames are a special case and go together under the
+        // StructuredName mime type
+        if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return StructuredName.CONTENT_ITEM_TYPE;
+        }
+        return mimeType;
+    }
+
+    /**
+     * Returns the DataKind for all entries in this List.
+     */
+    public DataKind getDataKind() {
+        return isEmpty() ? null : get(0).getDataKind();
+    }
+
+    /**
+     * Returns the "primary" KindSectionData and ValuesDelta that should be written for this List.
+     */
+    public Pair<KindSectionData,ValuesDelta> getEntryToWrite(AccountWithDataSet primaryAccount,
+            boolean hasNewContact) {
+        // Use the first writable contact that matches the primary account
+        if (primaryAccount != null && !hasNewContact) {
+            for (KindSectionData kindSectionData : this) {
+                if (kindSectionData.getAccountType().areContactsWritable()
+                        && !kindSectionData.getValuesDeltas().isEmpty()) {
+                    if (matchesAccount(primaryAccount, kindSectionData.getRawContactDelta())) {
+                        return new Pair<>(kindSectionData,
+                                kindSectionData.getValuesDeltas().get(0));
+                    }
+                }
+            }
+        }
+
+        // If no writable raw contact matched the primary account, or we're editing a read-only
+        // contact, just return the first writable entry.
+        for (KindSectionData kindSectionData : this) {
+            if (kindSectionData.getAccountType().areContactsWritable()) {
+                if (!kindSectionData.getValuesDeltas().isEmpty()) {
+                    return new Pair<>(kindSectionData, kindSectionData.getValuesDeltas().get(0));
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /** Whether the given RawContactDelta belong to the given account. */
+    private static boolean matchesAccount(AccountWithDataSet accountWithDataSet,
+            RawContactDelta rawContactDelta) {
+        if (accountWithDataSet == null) return false;
+        return Objects.equals(accountWithDataSet.name, rawContactDelta.getAccountName())
+                && Objects.equals(accountWithDataSet.type, rawContactDelta.getAccountType())
+                && Objects.equals(accountWithDataSet.dataSet, rawContactDelta.getDataSet());
+    }
+
+    /**
+     * Returns the "primary" KindSectionData and ValuesDelta that should be displayed to the user.
+     */
+    public Pair<KindSectionData,ValuesDelta> getEntryToDisplay(long id) {
+        final String mimeType = getMimeType();
+        if (mimeType == null) return null;
+
+        KindSectionData resultKindSectionData = null;
+        ValuesDelta resultValuesDelta = null;
+        if (id > 0) {
+            // Look for a match for the ID that was passed in
+            for (KindSectionData kindSectionData : this) {
+                resultValuesDelta = kindSectionData.getValuesDeltaById(id);
+                if (resultValuesDelta != null) {
+                    vlog(mimeType + ": matched kind section data by ID");
+                    resultKindSectionData = kindSectionData;
+                    break;
+                }
+            }
+        }
+        if (resultKindSectionData == null) {
+            // Look for a super primary entry
+            for (KindSectionData kindSectionData : this) {
+                resultValuesDelta = kindSectionData.getSuperPrimaryValuesDelta();
+                if (resultValuesDelta != null) {
+                    vlog(mimeType + ": matched super primary kind section data");
+                    resultKindSectionData = kindSectionData;
+                    break;
+                }
+            }
+        }
+        if (resultKindSectionData == null) {
+            // Fall back to the first non-empty value
+            for (KindSectionData kindSectionData : this) {
+                resultValuesDelta = kindSectionData.getFirstNonEmptyValuesDelta();
+                if (resultValuesDelta != null) {
+                    vlog(mimeType + ": using first non empty value");
+                    resultKindSectionData = kindSectionData;
+                    break;
+                }
+            }
+        }
+        if (resultKindSectionData == null || resultValuesDelta == null) {
+            final List<ValuesDelta> valuesDeltaList = get(0).getValuesDeltas();
+            if (valuesDeltaList != null && !valuesDeltaList.isEmpty()) {
+                vlog(mimeType + ": falling back to first empty entry");
+                resultValuesDelta = valuesDeltaList.get(0);
+                resultKindSectionData = get(0);
+            }
+        }
+        return resultKindSectionData != null && resultValuesDelta != null
+                ? new Pair<>(resultKindSectionData, resultValuesDelta) : null;
+    }
+
+    @Override
+    public boolean add(KindSectionData kindSectionData) {
+        if (kindSectionData == null) throw new NullPointerException();
+
+        // Enforce that only entries of the same type are added to this list
+        final String listMimeType = getMimeType();
+        if (listMimeType != null) {
+            final String newEntryMimeType = kindSectionData.getDataKind().mimeType;
+            if (isNameMimeType(listMimeType)) {
+                if (!isNameMimeType(newEntryMimeType)) {
+                    throw new IllegalArgumentException(
+                            "Can't add " + newEntryMimeType + " to list with type " + listMimeType);
+                }
+            } else if (!listMimeType.equals(newEntryMimeType)) {
+                throw new IllegalArgumentException(
+                        "Can't add " + newEntryMimeType + " to list with type " + listMimeType);
+            }
+        }
+        return super.add(kindSectionData);
+    }
+
+    // StructuredNames and Nicknames are a special case and go together
+    private static boolean isNameMimeType(String mimeType) {
+        return StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)
+                || Nickname.CONTENT_ITEM_TYPE.equals(mimeType);
+    }
+
+    private static void vlog(String message) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, message);
+        }
+    }
+}
diff --git a/src/com/android/contacts/editor/LabeledEditorView.java b/src/com/android/contacts/editor/LabeledEditorView.java
index 244b682..b68310a 100644
--- a/src/com/android/contacts/editor/LabeledEditorView.java
+++ b/src/com/android/contacts/editor/LabeledEditorView.java
@@ -380,7 +380,9 @@
             mWasEmpty = isEmpty;
 
             // Update the label text color
-            mEditTypeAdapter.notifyDataSetChanged();
+            if (mEditTypeAdapter != null) {
+                mEditTypeAdapter.notifyDataSetChanged();
+            }
         }
     }
 
diff --git a/src/com/android/contacts/editor/StructuredNameEditorView.java b/src/com/android/contacts/editor/StructuredNameEditorView.java
index 4cc8003..23cb4c1 100644
--- a/src/com/android/contacts/editor/StructuredNameEditorView.java
+++ b/src/com/android/contacts/editor/StructuredNameEditorView.java
@@ -225,6 +225,22 @@
         super.setValue(0, name);
     }
 
+    /**
+     * Returns the display name currently displayed in the editor.
+     */
+    public String getDisplayName() {
+        final ValuesDelta valuesDelta = getValues();
+        if (hasShortAndLongForms() && areOptionalFieldsVisible()) {
+            final Map<String, String> structuredNameMap = valuesToStructuredNameMap(valuesDelta);
+            final String displayName = NameConverter.structuredNameToDisplayName(
+                    getContext(), structuredNameMap);
+            if (!TextUtils.isEmpty(displayName)) {
+                return displayName;
+            }
+        }
+        return valuesDelta.getDisplayName();
+    }
+
     @Override
     protected Parcelable onSaveInstanceState() {
         SavedState state = new SavedState(super.onSaveInstanceState());