Combining data when switching to an aggregation suggestion

Now if you start creating a new contact and
enter some data, then follow an aggregation suggestion
by hitting "Edit" on it, the data you have entered
so far will be carried over to the suggested contact.

Also fixes bug: 3034215

Change-Id: Ic811a289a69fd6573b8735dcf2a7f5920332ce46
diff --git a/src/com/android/contacts/model/EntityModifier.java b/src/com/android/contacts/model/EntityModifier.java
index 70c88f4..8765a3f 100644
--- a/src/com/android/contacts/model/EntityModifier.java
+++ b/src/com/android/contacts/model/EntityModifier.java
@@ -27,6 +27,7 @@
 import android.content.Context;
 import android.database.Cursor;
 import android.os.Bundle;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.Intents;
 import android.provider.ContactsContract.RawContacts;
@@ -426,6 +427,24 @@
     }
 
     /**
+     * Compares corresponding fields in values1 and values2. Only the fields
+     * declared by the DataKind are taken into consideration.
+     */
+    protected static boolean areEqual(ValuesDelta values1, ContentValues values2, DataKind kind) {
+        if (kind.fieldList == null) return false;
+
+        for (EditField field : kind.fieldList) {
+            final String value1 = values1.getAsString(field.column);
+            final String value2 = values2.getAsString(field.column);
+            if (!TextUtils.equals(value1, value2)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
      * Parse the given {@link Bundle} into the given {@link EntityDelta} state,
      * assuming the extras defined through {@link Intents}.
      */
@@ -517,21 +536,188 @@
         }
 
         // Arbitrary additional data
-        {
-//            ArrayList<ContentValues> values = extras.getParcelableArrayList(Insert.DATA);
-//            if (values != null) {
-//                parseValues(state, values);
-//            }
+        ArrayList<ContentValues> values = extras.getParcelableArrayList(Insert.DATA);
+        if (values != null) {
+            parseValues(state, source, values);
         }
     }
 
-    private static void parseValues(EntityDelta state, ArrayList<ContentValues> values) {
-        for (ContentValues contentValues : values) {
-            String mimeType = contentValues.getAsString(Data.MIMETYPE);
+    private static void parseValues(
+            EntityDelta state, BaseAccountType source, ArrayList<ContentValues> dataValueList) {
+        for (ContentValues values : dataValueList) {
+            String mimeType = values.getAsString(Data.MIMETYPE);
+            if (TextUtils.isEmpty(mimeType)) {
+                Log.e(TAG, "Mimetype is required. Ignoring: " + values);
+                continue;
+            }
+
+            // Won't override the contact name
+            if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                continue;
+            }
+
+            DataKind kind = source.getKindForMimetype(mimeType);
+            if (kind == null) {
+                Log.e(TAG, "Mimetype not supported for account type " + source.accountType
+                        + ". Ignoring: " + values);
+                continue;
+            }
+
+            ValuesDelta entry = ValuesDelta.fromAfter(values);
+            if (isEmpty(entry, kind)) {
+                continue;
+            }
+
+            ArrayList<ValuesDelta> entries = state.getMimeEntries(mimeType);
+
+            if (kind.isList || GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                // Check for duplicates
+                boolean addEntry = true;
+                int count = 0;
+                if (entries != null && entries.size() > 0) {
+                    for (ValuesDelta delta : entries) {
+                        if (!delta.isDelete()) {
+                            if (areEqual(delta, values, kind)) {
+                                addEntry = false;
+                                break;
+                            }
+                            count++;
+                        }
+                    }
+                }
+
+                if (kind.typeOverallMax != -1 && count >= kind.typeOverallMax) {
+                    Log.e(TAG, "Mimetype allows at most " + kind.typeOverallMax
+                            + " entries. Ignoring: " + values);
+                    addEntry = false;
+                }
+
+                if (addEntry) {
+                    addEntry = adjustType(entry, entries, kind);
+                }
+
+                if (addEntry) {
+                    state.addEntry(entry);
+                }
+            } else {
+                // Non-list entries should not be overridden
+                boolean addEntry = true;
+                if (entries != null && entries.size() > 0) {
+                    for (ValuesDelta delta : entries) {
+                        if (!delta.isDelete() && !isEmpty(delta, kind)) {
+                            addEntry = false;
+                            break;
+                        }
+                    }
+                    if (addEntry) {
+                        for (ValuesDelta delta : entries) {
+                            delta.markDeleted();
+                        }
+                    }
+                }
+
+                if (addEntry) {
+                    addEntry = adjustType(entry, entries, kind);
+                }
+
+                if (addEntry) {
+                    state.addEntry(entry);
+                } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType)){
+                    // Note is most likely to contain large amounts of text
+                    // that we don't want to drop on the ground.
+                    for (ValuesDelta delta : entries) {
+                        if (!isEmpty(delta, kind)) {
+                            delta.put(Note.NOTE, delta.getAsString(Note.NOTE) + "\n"
+                                    + values.getAsString(Note.NOTE));
+                            break;
+                        }
+                    }
+                } else {
+                    Log.e(TAG, "Will not override mimetype " + mimeType + ". Ignoring: "
+                            + values);
+                }
+            }
         }
     }
 
     /**
+     * Checks if the data kind allows addition of another entry (e.g. Exchange only
+     * supports two "work" phone numbers).  If not, tries to switch to one of the
+     * unused types.  If successful, returns true.
+     */
+    private static boolean adjustType(
+            ValuesDelta entry, ArrayList<ValuesDelta> entries, DataKind kind) {
+        if (kind.typeColumn == null || kind.typeList == null || kind.typeList.size() == 0) {
+            return true;
+        }
+
+        Integer typeInteger = entry.getAsInteger(kind.typeColumn);
+        int type = typeInteger != null ? typeInteger : kind.typeList.get(0).rawValue;
+
+        if (isTypeAllowed(type, entries, kind)) {
+            entry.put(kind.typeColumn, type);
+            return true;
+        }
+
+        // Specified type is not allowed - choose the first available type that is allowed
+        int size = kind.typeList.size();
+        for (int i = 0; i < size; i++) {
+            EditType editType = kind.typeList.get(i);
+            if (isTypeAllowed(editType.rawValue, entries, kind)) {
+                entry.put(kind.typeColumn, editType.rawValue);
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Checks if a new entry of the specified type can be added to the raw
+     * contact. For example, Exchange only supports two "work" phone numbers, so
+     * addition of a third would not be allowed.
+     */
+    private static boolean isTypeAllowed(int type, ArrayList<ValuesDelta> entries, DataKind kind) {
+        int max = 0;
+        int size = kind.typeList.size();
+        for (int i = 0; i < size; i++) {
+            EditType editType = kind.typeList.get(i);
+            if (editType.rawValue == type) {
+                max = editType.specificMax;
+                break;
+            }
+        }
+
+        if (max == 0) {
+            // This type is not allowed at all
+            return false;
+        }
+
+        if (max == -1) {
+            // Unlimited instances of this type are allowed
+            return true;
+        }
+
+        return getEntryCountByType(entries, kind.typeColumn, type) < max;
+    }
+
+    /**
+     * Counts occurrences of the specified type in the supplied entry list.
+     */
+    private static int getEntryCountByType(
+            ArrayList<ValuesDelta> entries, String typeColumn, int type) {
+        int count = 0;
+        int size = entries.size();
+        for (int i = 0; i < size; i++) {
+            Integer typeInteger = entries.get(i).getAsInteger(typeColumn);
+            if (typeInteger != null && typeInteger == type) {
+                count++;
+            }
+        }
+        return count;
+    }
+
+    /**
      * Attempt to parse legacy {@link Insert#IM_PROTOCOL} values, replacing them
      * with updated values.
      */
diff --git a/src/com/android/contacts/views/editor/AggregationSuggestionEngine.java b/src/com/android/contacts/views/editor/AggregationSuggestionEngine.java
index 9b95aee..79131d6 100644
--- a/src/com/android/contacts/views/editor/AggregationSuggestionEngine.java
+++ b/src/com/android/contacts/views/editor/AggregationSuggestionEngine.java
@@ -125,7 +125,10 @@
     }
 
     public void setContactId(long contactId) {
-        mContactId = contactId;
+        if (contactId != mContactId) {
+            mContactId = contactId;
+            reset();
+        }
     }
 
     public void setListener(Listener listener) {
diff --git a/src/com/android/contacts/views/editor/ContactEditorFragment.java b/src/com/android/contacts/views/editor/ContactEditorFragment.java
index 46f7d38..3efca45 100644
--- a/src/com/android/contacts/views/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/views/editor/ContactEditorFragment.java
@@ -343,17 +343,21 @@
         mState = EntityDeltaList.fromIterator(entities.iterator());
 
         // Merge in Extras from Intent
-        final boolean hasExtras = mIntentExtras != null && mIntentExtras.size() > 0;
-        final boolean hasState = mState.size() > 0;
-        if (hasExtras && hasState) {
-            // Find source defining the first RawContact found
-            final EntityDelta state = mState.get(0);
-            final String accountType = state.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+        if (mIntentExtras != null && mIntentExtras.size() > 0) {
             final AccountTypes sources = AccountTypes.getInstance(mContext);
-            final BaseAccountType source = sources.getInflatedSource(accountType,
-                    BaseAccountType.LEVEL_CONSTRAINTS);
-            EntityModifier.parseExtras(mContext, source, state, mIntentExtras);
+            for (EntityDelta state : mState) {
+                final String accountType = state.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+                final BaseAccountType source = sources.getInflatedSource(accountType,
+                        BaseAccountType.LEVEL_CONSTRAINTS);
+                if (!source.readOnly) {
+                    // Apply extras to the first writable raw contact only
+                    EntityModifier.parseExtras(mContext, source, state, mIntentExtras);
+                    mIntentExtras = null;
+                    break;
+                }
+            }
         }
+
         bindEditors();
     }
 
@@ -1146,11 +1150,12 @@
 
         if (mAggregationSuggestionEngine == null) {
             mAggregationSuggestionEngine = new AggregationSuggestionEngine(getActivity());
-            mAggregationSuggestionEngine.setContactId(getContactId());
             mAggregationSuggestionEngine.setListener(this);
             mAggregationSuggestionEngine.start();
         }
 
+        mAggregationSuggestionEngine.setContactId(getContactId());
+
         FieldEditorView nameEditor = rawContactEditor.getNameEditor();
         mAggregationSuggestionEngine.onNameChange(nameEditor.getValues());
     }
@@ -1217,14 +1222,14 @@
                     mState = null;
 
                     // Load the suggested one
-                    load(Intent.ACTION_EDIT, contactLookupUri, Contacts.CONTENT_TYPE, null);
-                    mStatus = Status.LOADING;
                     Bundle extras = null;
                     if (values.size() != 0) {
                         extras = new Bundle();
-//                        extras.putParcelableArrayList(Insert.DATA, values);
+                        extras.putParcelableArrayList(Insert.DATA, values);
                     }
-                    getLoaderManager().restartLoader(LOADER_DATA, extras, mDataLoaderListener);
+                    load(Intent.ACTION_EDIT, contactLookupUri, Contacts.CONTENT_TYPE, extras);
+                    mStatus = Status.LOADING;
+                    getLoaderManager().restartLoader(LOADER_DATA, null, mDataLoaderListener);
                 }
             });
             suggestionView.bindSuggestion(suggestion);