Some minor improvements to Contact Editor design

- Can supply a custom editor class for a specific data kind
- Can have a field that fills up a row instead of leaving
  whitespace at the bottom
- Convenience method isChanged that checks if a specific
  field has been changed.

Change-Id: I7dcc4ba1a3100675362e1fc69b4afc3e6793f373
diff --git a/src/com/android/contacts/model/ContactsSource.java b/src/com/android/contacts/model/ContactsSource.java
index d498398..b417224 100644
--- a/src/com/android/contacts/model/ContactsSource.java
+++ b/src/com/android/contacts/model/ContactsSource.java
@@ -25,11 +25,12 @@
 import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.graphics.drawable.Drawable;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.RawContacts;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.view.View;
 import android.widget.EditText;
 
 import java.util.ArrayList;
@@ -221,10 +222,17 @@
 
         public ContentValues defaultValues;
 
+        public Class<? extends View> editorClass;
+
         public DataKind() {
         }
 
         public DataKind(String mimeType, int titleRes, int iconRes, int weight, boolean editable) {
+            this(mimeType, titleRes, iconRes, weight, editable, null);
+        }
+
+        public DataKind(String mimeType, int titleRes, int iconRes, int weight, boolean editable,
+                Class<? extends View> editorClass) {
             this.mimeType = mimeType;
             this.titleRes = titleRes;
             this.iconRes = iconRes;
@@ -232,6 +240,7 @@
             this.editable = editable;
             this.isList = true;
             this.typeOverallMax = -1;
+            this.editorClass = editorClass;
         }
     }
 
@@ -324,6 +333,11 @@
             this.longForm = longForm;
             return this;
         }
+
+        public EditField setMinLines(int minLines) {
+            this.minLines = minLines;
+            return this;
+        }
     }
 
     /**
diff --git a/src/com/android/contacts/model/Editor.java b/src/com/android/contacts/model/Editor.java
index 04e023b..c73839d 100644
--- a/src/com/android/contacts/model/Editor.java
+++ b/src/com/android/contacts/model/Editor.java
@@ -58,6 +58,8 @@
     public void setValues(DataKind kind, ValuesDelta values, EntityDelta state, boolean readOnly,
             ViewIdGenerator vig);
 
+    public void setDeletable(boolean deletable);
+
     /**
      * Add a specific {@link EditorListener} to this {@link Editor}.
      */
diff --git a/src/com/android/contacts/model/EntityDelta.java b/src/com/android/contacts/model/EntityDelta.java
index 4f018a9..e353d70 100644
--- a/src/com/android/contacts/model/EntityDelta.java
+++ b/src/com/android/contacts/model/EntityDelta.java
@@ -578,6 +578,21 @@
             }
         }
 
+        public boolean isChanged(String key) {
+            if (mAfter == null || !mAfter.containsKey(key)) {
+                return false;
+            }
+
+            Object newValue = mAfter.get(key);
+            Object oldValue = mBefore.get(key);
+
+            if (oldValue == null) {
+                return newValue != null;
+            }
+
+            return !oldValue.equals(newValue);
+        }
+
         public String getMimetype() {
             return getAsString(Data.MIMETYPE);
         }
diff --git a/src/com/android/contacts/ui/widget/GenericEditorView.java b/src/com/android/contacts/ui/widget/GenericEditorView.java
index b5e0c4f..6be238f 100644
--- a/src/com/android/contacts/ui/widget/GenericEditorView.java
+++ b/src/com/android/contacts/ui/widget/GenericEditorView.java
@@ -43,6 +43,7 @@
 import android.text.TextWatcher;
 import android.util.AttributeSet;
 import android.view.ContextThemeWrapper;
+import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -164,13 +165,38 @@
 
         // summarize the EditText heights
         int totalHeight = 0;
+        int visibleFieldCount = 0;
+        EditText firstVisibleField = null;
         if (mFieldEditTexts != null) {
             for (EditText editText : mFieldEditTexts) {
                 if (editText.getVisibility() != View.GONE) {
+                    visibleFieldCount ++;
+                    if (firstVisibleField == null) {
+                        firstVisibleField = editText;
+                    }
                     totalHeight += editText.getMeasuredHeight();
                 }
             }
         }
+
+        int padding = getPaddingTop() + getPaddingBottom();
+        int minHeight = padding;
+
+        if (mMoreOrLess != null) {
+            minHeight += mMoreOrLess.getMeasuredHeight();
+        }
+
+        if (mDelete != null) {
+            minHeight += mDelete.getMeasuredHeight();
+        }
+
+        if (minHeight > totalHeight && visibleFieldCount == 1) {
+            firstVisibleField.measure(widthMeasureSpec,
+                    MeasureSpec.makeMeasureSpec(minHeight - padding, MeasureSpec.EXACTLY));
+        }
+
+        totalHeight = Math.max(minHeight, totalHeight);
+
         setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                 resolveSize(totalHeight, heightMeasureSpec));
     }
@@ -259,9 +285,7 @@
 
                         // Reconfigure GUI
                         mHideOptional = !mHideOptional;
-                        if (mListener != null) {
-                            mListener.onRequest(EditorListener.EDITOR_FORM_CHANGED);
-                        }
+                        onOptionalFieldVisibilityChange();
                         rebuildValues();
 
                         // Restore focus
@@ -285,6 +309,12 @@
         }
     }
 
+    protected void onOptionalFieldVisibilityChange() {
+        if (mListener != null) {
+            mListener.onRequest(EditorListener.EDITOR_FORM_CHANGED);
+        }
+    }
+
     public void setEditorListener(EditorListener listener) {
         mListener = listener;
     }
@@ -398,6 +428,7 @@
             final EditText fieldView = new EditText(mContext);
             fieldView.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT,
                     LayoutParams.WRAP_CONTENT));
+            fieldView.setGravity(Gravity.TOP);
             mFieldEditTexts[index] = fieldView;
             fieldView.setId(vig.getId(state, kind, entry, index));
             if (field.titleRes > 0) {
diff --git a/src/com/android/contacts/ui/widget/KindSectionView.java b/src/com/android/contacts/ui/widget/KindSectionView.java
index cb20566..cd0b6fb 100644
--- a/src/com/android/contacts/ui/widget/KindSectionView.java
+++ b/src/com/android/contacts/ui/widget/KindSectionView.java
@@ -17,12 +17,12 @@
 package com.android.contacts.ui.widget;
 
 import com.android.contacts.R;
-import com.android.contacts.model.Editor;
-import com.android.contacts.model.EntityDelta;
-import com.android.contacts.model.EntityModifier;
 import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.model.Editor;
 import com.android.contacts.model.Editor.EditorListener;
+import com.android.contacts.model.EntityDelta;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.model.EntityModifier;
 import com.android.contacts.ui.ViewIdGenerator;
 
 import android.content.Context;
@@ -137,13 +137,27 @@
                 if (!entry.isVisible()) continue;
                 if (isEmptyNoop(entry)) continue;
 
-                final GenericEditorView editor = new GenericEditorView(mContext);
+                final View view;
+                if (mKind.editorClass == null) {
+                    view = new GenericEditorView(mContext);
+                } else {
+                    try {
+                        view = mKind.editorClass.getConstructor(Context.class).newInstance(
+                                mContext);
+                    } catch (Exception e) {
+                        throw new RuntimeException(
+                                "Cannot allocate editor for " + mKind.editorClass);
+                    }
+                }
 
-                editor.setPadding(0, 0, getThemeScrollbarSize(mContext), 0);
-                editor.setValues(mKind, entry, mState, mReadOnly, mViewIdGenerator);
-                editor.setEditorListener(this);
-                editor.setDeletable(true);
-                mEditors.addView(editor);
+                view.setPadding(0, 0, getThemeScrollbarSize(mContext), 0);
+                if (view instanceof Editor) {
+                    Editor editor = (Editor) view;
+                    editor.setValues(mKind, entry, mState, mReadOnly, mViewIdGenerator);
+                    editor.setEditorListener(this);
+                    editor.setDeletable(true);
+                }
+                mEditors.addView(view);
                 entryIndex++;
             }
         }
diff --git a/src/com/android/contacts/ui/widget/PhotoEditorView.java b/src/com/android/contacts/ui/widget/PhotoEditorView.java
index eff39d0..da1be85 100644
--- a/src/com/android/contacts/ui/widget/PhotoEditorView.java
+++ b/src/com/android/contacts/ui/widget/PhotoEditorView.java
@@ -168,4 +168,9 @@
     public void setEditorListener(EditorListener listener) {
         mListener = listener;
     }
+
+    @Override
+    public void setDeletable(boolean deletable) {
+        // Photo is not deletable
+    }
 }