Merge change 24599 into eclair

* changes:
  [Issue 2112887] Fixing picture display in the manual contact join UI
diff --git a/res/layout/item_photo_editor.xml b/res/layout/item_photo_editor.xml
index b2a5f33..7544439 100644
--- a/res/layout/item_photo_editor.xml
+++ b/res/layout/item_photo_editor.xml
@@ -21,6 +21,7 @@
     android:clickable="true"
     android:focusable="true"
     android:src="@drawable/ic_menu_add_picture"
+    android:cropToPadding="true"
     android:scaleType="center"
     android:background="@drawable/btn_contact_picture"
     android:gravity="center" />
diff --git a/src/com/android/contacts/model/Editor.java b/src/com/android/contacts/model/Editor.java
index 64c0952..d6f7003 100644
--- a/src/com/android/contacts/model/Editor.java
+++ b/src/com/android/contacts/model/Editor.java
@@ -34,6 +34,14 @@
          * Called when the given {@link Editor} has been deleted.
          */
         public void onDeleted(Editor editor);
+
+        /**
+         * Called when the given {@link Editor} has a request, for example it
+         * wants to select a photo.
+         */
+        public void onRequest(int request);
+
+        public static final int REQUEST_PICK_PHOTO = 1;
     }
 
     /**
diff --git a/src/com/android/contacts/model/EntityDelta.java b/src/com/android/contacts/model/EntityDelta.java
index f096cc7..f221247 100644
--- a/src/com/android/contacts/model/EntityDelta.java
+++ b/src/com/android/contacts/model/EntityDelta.java
@@ -551,6 +551,11 @@
             mAfter.put(key, value);
         }
 
+        public void put(String key, byte[] value) {
+            ensureUpdate();
+            mAfter.put(key, value);
+        }
+
         public void put(String key, int value) {
             ensureUpdate();
             mAfter.put(key, value);
diff --git a/src/com/android/contacts/model/EntityModifier.java b/src/com/android/contacts/model/EntityModifier.java
index e904a8b..a0a54ef 100644
--- a/src/com/android/contacts/model/EntityModifier.java
+++ b/src/com/android/contacts/model/EntityModifier.java
@@ -17,6 +17,7 @@
 package com.android.contacts.model;
 
 import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.model.ContactsSource.EditField;
 import com.android.contacts.model.ContactsSource.EditType;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
 import com.google.android.collect.Lists;
@@ -34,6 +35,7 @@
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.provider.ContactsContract.Intents.Insert;
 import android.text.TextUtils;
+import android.util.Log;
 import android.util.SparseIntArray;
 
 import java.util.ArrayList;
@@ -45,6 +47,8 @@
  * new rows, or enforcing {@link ContactsSource}.
  */
 public class EntityModifier {
+    private static final String TAG = "EntityModifier";
+
     /**
      * For the given {@link EntityDelta}, determine if the given
      * {@link DataKind} could be inserted under specific
@@ -335,6 +339,48 @@
     }
 
     /**
+     * Processing to trim any empty {@link ValuesDelta} rows from the given
+     * {@link EntityDelta}, assuming the given {@link ContactsSource} dictates
+     * the structure for various fields. This method ignores rows not described
+     * by the {@link ContactsSource}.
+     */
+    public static void trimEmpty(ContactsSource source, EntityDelta state) {
+        // Walk through entries for each well-known kind
+        for (DataKind kind : source.getSortedDataKinds()) {
+            final String mimeType = kind.mimeType;
+            final ArrayList<ValuesDelta> entries = state.getMimeEntries(mimeType);
+            if (entries == null) continue;
+
+            for (ValuesDelta entry : entries) {
+                // Test and remove this row if empty
+                final boolean touched = entry.isInsert() || entry.isUpdate();
+                if (touched && EntityModifier.isEmpty(entry, kind)) {
+                    // TODO: remove this verbose logging
+                    Log.w(TAG, "Trimming: " + entry.toString());
+                    entry.markDeleted();
+                }
+            }
+        }
+    }
+
+    /**
+     * Test if the given {@link ValuesDelta} would be considered "empty" in
+     * terms of {@link DataKind#fieldList}.
+     */
+    public static boolean isEmpty(ValuesDelta values, DataKind kind) {
+        boolean hasValues = false;
+        for (EditField field : kind.fieldList) {
+            // If any field has values, we're not empty
+            final String value = values.getAsString(field.column);
+            if (!TextUtils.isEmpty(value)) {
+                hasValues = true;
+            }
+        }
+
+        return !hasValues;
+    }
+
+    /**
      * Parse the given {@link Bundle} into the given {@link EntityDelta} state,
      * assuming the extras defined through {@link Intents}.
      */
diff --git a/src/com/android/contacts/model/EntitySet.java b/src/com/android/contacts/model/EntitySet.java
index f9425b9..b9a71e7 100644
--- a/src/com/android/contacts/model/EntitySet.java
+++ b/src/com/android/contacts/model/EntitySet.java
@@ -159,7 +159,7 @@
      * existing {@link RawContacts#_ID} value. Usually used when creating
      * {@link AggregationExceptions} during an update.
      */
-    public long findRawContactId() {
+    protected long findRawContactId() {
         for (EntityDelta delta : this) {
             final Long rawContactId = delta.getValues().getAsLong(RawContacts._ID);
             if (rawContactId != null && rawContactId >= 0) {
@@ -177,10 +177,24 @@
             final EntityDelta delta = this.get(index);
             return delta.getValues().getAsLong(RawContacts._ID);
         } else {
-            return -1;
+            return 0;
         }
     }
 
+    /**
+     * Find index of given {@link RawContacts#_ID} when present.
+     */
+    public int indexOfRawContactId(long rawContactId) {
+        final int size = this.size();
+        for (int i = 0; i < size; i++) {
+            final long currentId = getRawContactId(i);
+            if (currentId == rawContactId) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
     /** {@inheritDoc} */
     public int describeContents() {
         // Nothing special about this parcel
diff --git a/src/com/android/contacts/model/HardCodedSources.java b/src/com/android/contacts/model/HardCodedSources.java
index 9f91a74..03dd226 100644
--- a/src/com/android/contacts/model/HardCodedSources.java
+++ b/src/com/android/contacts/model/HardCodedSources.java
@@ -161,6 +161,10 @@
         {
             // GOOGLE: PHOTO
             DataKind kind = new DataKind(Photo.CONTENT_ITEM_TYPE, -1, -1, -1, true);
+
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));
+
             list.add(kind);
         }
 
@@ -445,6 +449,10 @@
             // EXCHANGE: PHOTO
             DataKind kind = new DataKind(Photo.CONTENT_ITEM_TYPE, -1, -1, -1, true);
             kind.typeOverallMax = 1;
+
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));
+
             list.add(kind);
         }
 
diff --git a/src/com/android/contacts/ui/EditContactActivity.java b/src/com/android/contacts/ui/EditContactActivity.java
index 7e73568..eda1729 100644
--- a/src/com/android/contacts/ui/EditContactActivity.java
+++ b/src/com/android/contacts/ui/EditContactActivity.java
@@ -21,11 +21,13 @@
 import com.android.contacts.ScrollingTabWidget;
 import com.android.contacts.ViewContactActivity;
 import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.Editor;
 import com.android.contacts.model.EntityDelta;
 import com.android.contacts.model.EntityModifier;
 import com.android.contacts.model.EntitySet;
 import com.android.contacts.model.HardCodedSources;
 import com.android.contacts.model.Sources;
+import com.android.contacts.model.Editor.EditorListener;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
 import com.android.contacts.ui.widget.ContactEditorView;
 import com.android.contacts.util.EmptyService;
@@ -47,6 +49,7 @@
 import android.content.Entity;
 import android.content.Intent;
 import android.content.OperationApplicationException;
+import android.graphics.Bitmap;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.RemoteException;
@@ -80,7 +83,8 @@
  * Activity for editing or inserting a contact.
  */
 public final class EditContactActivity extends Activity implements View.OnClickListener,
-        ScrollingTabWidget.OnTabSelectionChangedListener, ContactHeaderWidget.ContactHeaderListener {
+        ScrollingTabWidget.OnTabSelectionChangedListener,
+        ContactHeaderWidget.ContactHeaderListener, EditorListener {
     private static final String TAG = "EditContactActivity";
 
     /** The launch code when picking a photo and the raw data is returned */
@@ -89,7 +93,8 @@
     private static final int TOKEN_ENTITY = 41;
 
     private static final String KEY_EDIT_STATE = "state";
-    private static final String KEY_SELECTED_TAB = "tab";
+    private static final String KEY_SELECTED_RAW_CONTACT = "selected";
+
 //    private static final String KEY_SELECTED_TAB_ID = "tabId";
 //    private static final String KEY_CONTACT_ID = "contactId";
 
@@ -128,7 +133,9 @@
         mTabWidget = (ScrollingTabWidget)this.findViewById(R.id.tab_widget);
         mTabWidget.setTabSelectionListener(this);
 
+        // Build editor and listen for photo requests
         mEditor = (ContactEditorView)this.findViewById(android.R.id.tabcontent);
+        mEditor.getPhotoEditor().setEditorListener(this);
 
         findViewById(R.id.btn_done).setOnClickListener(this);
         findViewById(R.id.btn_discard).setOnClickListener(this);
@@ -140,7 +147,6 @@
         } else if (Intent.ACTION_INSERT.equals(action) && icicle == null) {
             // Trigger dialog to pick account type
             doAddAction();
-
         }
     }
 
@@ -195,28 +201,11 @@
 
 
 
-//    /**
-//     * Instance state for {@link #mEditor} from a previous instance.
-//     */
-//    private SparseArray<Parcelable> mEditorState;
-//
-//    /**
-//     * Save state of the currently selected {@link #mEditor}, usually for
-//     * passing across instance boundaries to restore later.
-//     */
-//    private SparseArray<Parcelable> buildEditorState() {
-//        final SparseArray<Parcelable> state = new SparseArray<Parcelable>();
-//        if (mEditor != null) {
-//            mEditor.getView().saveHierarchyState(state);
-//        }
-//        return state;
-//    }
-//
     @Override
     protected void onSaveInstanceState(Bundle outState) {
         // Store entities with modifications
         outState.putParcelable(KEY_EDIT_STATE, mState);
-        outState.putInt(KEY_SELECTED_TAB, mTabWidget.getCurrentTab());
+        outState.putLong(KEY_SELECTED_RAW_CONTACT, getSelectedRawContactId());
 //        outState.putLong(KEY_SELECTED_TAB_ID, mSelectedRawContactId);
 //        outState.putLong(KEY_CONTACT_ID, mContactId);
 
@@ -236,13 +225,30 @@
         bindTabs();
         bindHeader();
 
-        final int selectedTab = savedInstanceState.getInt(KEY_SELECTED_TAB);
-        mTabWidget.setCurrentTab(selectedTab);
+        final long selectedId = savedInstanceState.getLong(KEY_SELECTED_RAW_CONTACT);
+        setSelectedRawContactId(selectedId);
 
         // Restore selected tab and any focus
         super.onRestoreInstanceState(savedInstanceState);
     }
 
+    /**
+     * Return the {@link RawContacts#_ID} of the currently selected tab.
+     */
+    protected long getSelectedRawContactId() {
+        final int index = mTabWidget.getCurrentTab();
+        return mState.getRawContactId(index);
+    }
+
+    /**
+     * Set the selected tab based on the given {@link RawContacts#_ID}.
+     */
+    protected void setSelectedRawContactId(long rawContactId) {
+        final int index = mState.indexOfRawContactId(rawContactId);
+        mTabWidget.setCurrentTab(index);
+    }
+
+
 
     /**
      * Rebuild tabs to match our underlying {@link #mState} object, usually
@@ -357,16 +363,11 @@
 
         switch (requestCode) {
             case PHOTO_PICKED_WITH_DATA: {
-                // TODO: pass back to requesting tab
-//                final Bundle extras = data.getExtras();
-//                if (extras != null) {
-//                    Bitmap photo = extras.getParcelable("data");
-//                    mPhoto = photo;
-//                    mPhotoChanged = true;
-//                    mPhotoImageView.setImageBitmap(photo);
-//                    setPhotoPresent(true);
-//                }
-//                break;
+                // When reaching this point, we've already inflated our tab
+                // state and returned to the last-visible tab.
+                final Bitmap photo = data.getParcelableExtra("data");
+                mEditor.setPhotoBitmap(photo);
+                break;
             }
         }
     }
@@ -384,11 +385,11 @@
 
     @Override
     public boolean onPrepareOptionsMenu(Menu menu) {
-        // TODO: show or hide photo items based on current tab
-        // hide photo stuff entirely if on read-only source
+        final boolean hasPhotoEditor = mEditor.hasPhotoEditor();
+        final boolean hasSetPhoto = mEditor.hasSetPhoto();
 
-        menu.findItem(R.id.menu_photo_add).setVisible(false);
-        menu.findItem(R.id.menu_photo_remove).setVisible(false);
+        menu.findItem(R.id.menu_photo_add).setVisible(hasPhotoEditor);
+        menu.findItem(R.id.menu_photo_remove).setVisible(hasSetPhoto);
 
         return true;
     }
@@ -567,27 +568,46 @@
         return true;
     }
 
-
     /**
-     * Pick a specific photo to be added under this contact.
+     * Pick a specific photo to be added under the currently selected tab.
      */
     private boolean doPickPhotoAction() {
         try {
+            // Launch picker to choose photo for selected contact
             final Intent intent = ContactsUtils.getPhotoPickIntent();
             startActivityForResult(intent, PHOTO_PICKED_WITH_DATA);
         } catch (ActivityNotFoundException e) {
-            new AlertDialog.Builder(EditContactActivity.this).setTitle(R.string.errorDialogTitle)
-                    .setMessage(R.string.photoPickerNotFoundText).setPositiveButton(
-                            android.R.string.ok, null).show();
+            Toast.makeText(this, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
         }
         return true;
     }
 
+    /**
+     * Clear any existing photo under the currently selected tab.
+     */
     public boolean doRemovePhotoAction() {
-        // TODO: remove photo from current contact
+        // Remove photo from selected contact
+        mEditor.setPhotoBitmap(null);
         return true;
     }
 
+    /** {@inheritDoc} */
+    public void onDeleted(Editor editor) {
+        // Ignore any editor deletes
+    }
+
+    /** {@inheritDoc} */
+    public void onRequest(int request) {
+        switch (request) {
+            case EditorListener.REQUEST_PICK_PHOTO: {
+                doPickPhotoAction();
+                break;
+            }
+        }
+    }
+
+
+
 
 
 
diff --git a/src/com/android/contacts/ui/widget/ContactEditorView.java b/src/com/android/contacts/ui/widget/ContactEditorView.java
index 1fc935d..b35eb40 100644
--- a/src/com/android/contacts/ui/widget/ContactEditorView.java
+++ b/src/com/android/contacts/ui/widget/ContactEditorView.java
@@ -21,43 +21,27 @@
 import com.android.contacts.model.EntityDelta;
 import com.android.contacts.model.EntityModifier;
 import com.android.contacts.model.ContactsSource.DataKind;
-import com.android.contacts.model.ContactsSource.EditField;
 import com.android.contacts.model.ContactsSource.EditType;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
 
-import android.app.AlertDialog;
-import android.app.Dialog;
 import android.content.Context;
-import android.content.DialogInterface;
 import android.content.Entity;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
 import android.graphics.drawable.Drawable;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
 import android.provider.ContactsContract.CommonDataKinds.Photo;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
-import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
-import android.text.Editable;
-import android.text.TextUtils;
-import android.text.TextWatcher;
 import android.util.AttributeSet;
-import android.view.ContextThemeWrapper;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.View.OnClickListener;
-import android.view.inputmethod.EditorInfo;
-import android.widget.ArrayAdapter;
-import android.widget.EditText;
-import android.widget.ImageView;
-import android.widget.ListAdapter;
 import android.widget.RelativeLayout;
 import android.widget.TextView;
 
-import java.util.List;
-
 /**
  * Custom view that provides all the editor interaction for a specific
  * {@link Contacts} represented through an {@link EntityDelta}. Callers can
@@ -75,6 +59,8 @@
     private PhotoEditorView mPhoto;
     private GenericEditorView mName;
 
+    private boolean mHasPhotoEditor = false;
+
     private ViewGroup mGeneral;
     private ViewGroup mSecondary;
 
@@ -94,6 +80,8 @@
     /** {@inheritDoc} */
     @Override
     protected void onFinishInflate() {
+        super.onFinishInflate();
+
         mInflater = (LayoutInflater)getContext().getSystemService(
                 Context.LAYOUT_INFLATER_SERVICE);
 
@@ -118,6 +106,33 @@
         this.setSecondaryVisible(false);
     }
 
+    /**
+     * Assign the given {@link Bitmap} to the internal {@link PhotoEditorView}
+     * for the {@link EntityDelta} currently being edited.
+     */
+    public void setPhotoBitmap(Bitmap bitmap) {
+        mPhoto.setPhotoBitmap(bitmap);
+    }
+
+    /**
+     * Return true if the current {@link RawContacts} supports {@link Photo},
+     * which means that {@link PhotoEditorView} is enabled.
+     */
+    public boolean hasPhotoEditor() {
+        return mHasPhotoEditor;
+    }
+
+    /**
+     * Return true if internal {@link PhotoEditorView} has a {@link Photo} set.
+     */
+    public boolean hasSetPhoto() {
+        return mPhoto.hasSetPhoto();
+    }
+
+    public PhotoEditorView getPhotoEditor() {
+        return mPhoto;
+    }
+
     /** {@inheritDoc} */
     public void onClick(View v) {
         // Toggle visibility of secondary kinds
@@ -150,6 +165,11 @@
         // Make sure we have StructuredName
         EntityModifier.ensureKindExists(state, source, StructuredName.CONTENT_ITEM_TYPE);
 
+        // Show photo editor when supported
+        EntityModifier.ensureKindExists(state, source, Photo.CONTENT_ITEM_TYPE);
+        mHasPhotoEditor = (source.getKindForMimetype(Photo.CONTENT_ITEM_TYPE) != null);
+        mPhoto.setVisibility(mHasPhotoEditor ? View.VISIBLE : View.GONE);
+
         // Create editor sections for each possible data kind
         for (DataKind kind : source.getSortedDataKinds()) {
             // Skip kind of not editable
diff --git a/src/com/android/contacts/ui/widget/KindSectionView.java b/src/com/android/contacts/ui/widget/KindSectionView.java
index 14ec349..5a63992 100644
--- a/src/com/android/contacts/ui/widget/KindSectionView.java
+++ b/src/com/android/contacts/ui/widget/KindSectionView.java
@@ -50,7 +50,7 @@
 
     private DataKind mKind;
     private EntityDelta mState;
-    
+
     public KindSectionView(Context context) {
         super(context);
     }
@@ -76,11 +76,17 @@
         mTitle = (TextView)findViewById(R.id.kind_title);
     }
 
+    /** {@inheritDoc} */
     public void onDeleted(Editor editor) {
         this.updateAddEnabled();
         this.updateEditorsVisible();
     }
 
+    /** {@inheritDoc} */
+    public void onRequest(int request) {
+        // Ignore requests
+    }
+
     public void setState(DataKind kind, EntityDelta state) {
         mKind = kind;
         mState = state;
@@ -125,7 +131,8 @@
         final boolean canInsert = EntityModifier.canInsert(mState, mKind);
         mAdd.setEnabled(canInsert);
     }
-    
+
+    /** {@inheritDoc} */
     public void onClick(View v) {
         // Insert a new child and rebuild
         EntityModifier.insertChild(mState, mKind);
diff --git a/src/com/android/contacts/ui/widget/PhotoEditorView.java b/src/com/android/contacts/ui/widget/PhotoEditorView.java
index b88dc3b..cde314d 100644
--- a/src/com/android/contacts/ui/widget/PhotoEditorView.java
+++ b/src/com/android/contacts/ui/widget/PhotoEditorView.java
@@ -27,13 +27,24 @@
 import android.graphics.BitmapFactory;
 import android.provider.ContactsContract.CommonDataKinds.Photo;
 import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
 import android.widget.ImageView;
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
 /**
  * Simple editor for {@link Photo}.
  */
-public class PhotoEditorView extends ImageView implements Editor {
+public class PhotoEditorView extends ImageView implements Editor, OnClickListener {
+    private static final String TAG = "PhotoEditorView";
+
     private ValuesDelta mEntry;
+    private EditorListener mListener;
+
+    private boolean mHasSetPhoto = false;
 
     public PhotoEditorView(Context context) {
         super(context);
@@ -44,10 +55,25 @@
     }
 
     /** {@inheritDoc} */
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        this.setOnClickListener(this);
+    }
+
+    /** {@inheritDoc} */
+    public void onClick(View v) {
+        if (mListener != null) {
+            mListener.onRequest(EditorListener.REQUEST_PICK_PHOTO);
+        }
+    }
+
+    /** {@inheritDoc} */
     public void onFieldChanged(String column, String value) {
         throw new UnsupportedOperationException("Photos don't support direct field changes");
     }
 
+    /** {@inheritDoc} */
     public void setValues(DataKind kind, ValuesDelta values, EntityDelta state) {
         mEntry = values;
         if (values != null) {
@@ -59,6 +85,7 @@
 
                 setScaleType(ImageView.ScaleType.CENTER_CROP);
                 setImageBitmap(photo);
+                mHasSetPhoto = true;
             } else {
                 resetDefault();
             }
@@ -67,12 +94,50 @@
         }
     }
 
-    protected void resetDefault() {
-        // Invalid photo, show default "add photo" placeholder
-        setScaleType(ImageView.ScaleType.CENTER);
-        setImageResource(R.drawable.ic_menu_add_picture);
+    /**
+     * Return true if a valid {@link Photo} has been set.
+     */
+    public boolean hasSetPhoto() {
+        return mHasSetPhoto;
     }
 
+    /**
+     * Assign the given {@link Bitmap} as the new value, updating UI and
+     * readying for persisting through {@link ValuesDelta}.
+     */
+    public void setPhotoBitmap(Bitmap photo) {
+        if (photo == null) {
+            // Clear any existing photo and return
+            mEntry.put(Photo.PHOTO, (byte[])null);
+            resetDefault();
+            return;
+        }
+
+        final int size = photo.getWidth() * photo.getHeight() * 4;
+        final ByteArrayOutputStream out = new ByteArrayOutputStream(size);
+
+        try {
+            photo.compress(Bitmap.CompressFormat.PNG, 100, out);
+            out.flush();
+            out.close();
+
+            mEntry.put(Photo.PHOTO, out.toByteArray());
+            setImageBitmap(photo);
+            mHasSetPhoto = true;
+        } catch (IOException e) {
+            Log.w(TAG, "Unable to serialize photo: " + e.toString());
+        }
+    }
+
+    protected void resetDefault() {
+        // Invalid photo, show default "add photo" place-holder
+        setScaleType(ImageView.ScaleType.CENTER);
+        setImageResource(R.drawable.ic_menu_add_picture);
+        mHasSetPhoto = false;
+    }
+
+    /** {@inheritDoc} */
     public void setEditorListener(EditorListener listener) {
+        mListener = listener;
     }
 }
diff --git a/tests/src/com/android/contacts/EntityModifierTests.java b/tests/src/com/android/contacts/EntityModifierTests.java
index b72ee19..9d0c1be 100644
--- a/tests/src/com/android/contacts/EntityModifierTests.java
+++ b/tests/src/com/android/contacts/EntityModifierTests.java
@@ -16,20 +16,31 @@
 
 package com.android.contacts;
 
-import com.android.contacts.model.EntityDelta;
+import static android.content.ContentProviderOperation.TYPE_DELETE;
+import static android.content.ContentProviderOperation.TYPE_INSERT;
+import static android.content.ContentProviderOperation.TYPE_UPDATE;
+
 import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.EntityDelta;
 import com.android.contacts.model.EntityModifier;
 import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.model.ContactsSource.EditField;
 import com.android.contacts.model.ContactsSource.EditType;
+import com.android.contacts.model.EntityDelta.ValuesDelta;
 import com.google.android.collect.Lists;
 
+import android.content.ContentProviderOperation;
 import android.content.ContentValues;
 import android.content.Entity;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 /**
@@ -40,6 +51,9 @@
 public class EntityModifierTests extends AndroidTestCase {
     public static final String TAG = "EntityModifierTests";
 
+    private static final long TEST_ID = 4;
+    private static final String TEST_PHONE = "218-555-1212";
+
     public EntityModifierTests() {
         super();
     }
@@ -69,6 +83,10 @@
             kind.typeList.add(new EditType(Phone.TYPE_FAX_WORK, -1).setSecondary(true));
             kind.typeList.add(new EditType(Phone.TYPE_OTHER, -1));
 
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(Phone.NUMBER, -1, -1));
+            kind.fieldList.add(new EditField(Phone.LABEL, -1, -1));
+
             list.add(kind);
         }
 
@@ -78,9 +96,13 @@
     /**
      * Build an {@link Entity} with the requested set of phone numbers.
      */
-    protected EntityDelta getEntity() {
+    protected EntityDelta getEntity(ContentValues... entries) {
         final ContentValues contact = new ContentValues();
+        contact.put(RawContacts._ID, TEST_ID);
         final Entity before = new Entity(contact);
+        for (ContentValues values : entries) {
+            before.addSubValue(Data.CONTENT_URI, values);
+        }
         return EntityDelta.fromBefore(before);
     }
 
@@ -212,4 +234,154 @@
         suggested = EntityModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
         assertEquals("Unexpected suggestion", typeOther, suggested);
     }
+
+    public void testIsEmptyEmpty() {
+        final ContactsSource source = getSource();
+        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+
+        // Test entirely empty row
+        final ContentValues after = new ContentValues();
+        final ValuesDelta values = ValuesDelta.fromAfter(after);
+
+        assertTrue("Expected empty", EntityModifier.isEmpty(values, kindPhone));
+    }
+
+    public void testIsEmptyDirectFields() {
+        final ContactsSource source = getSource();
+        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+        final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+        // Test row that has type values, but core fields are empty
+        final EntityDelta state = getEntity();
+        final ValuesDelta values = EntityModifier.insertChild(state, kindPhone, typeHome);
+
+        assertTrue("Expected empty", EntityModifier.isEmpty(values, kindPhone));
+
+        // Insert some data to trigger non-empty state
+        values.put(Phone.NUMBER, TEST_PHONE);
+
+        assertFalse("Expected non-empty", EntityModifier.isEmpty(values, kindPhone));
+    }
+
+    public void testTrimEmptySingle() {
+        final ContactsSource source = getSource();
+        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+        final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+        // Test row that has type values, but core fields are empty
+        final EntityDelta state = getEntity();
+        final ValuesDelta values = EntityModifier.insertChild(state, kindPhone, typeHome);
+
+        // Build diff, expecting insert for data row and update enforcement
+        final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+        state.buildDiff(diff);
+        assertEquals("Unexpected operations", 3, diff.size());
+        {
+            final ContentProviderOperation oper = diff.get(0);
+            assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+        {
+            final ContentProviderOperation oper = diff.get(1);
+            assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
+            assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+        }
+        {
+            final ContentProviderOperation oper = diff.get(2);
+            assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+
+        // Trim empty rows and try again, expecting no changes
+        EntityModifier.trimEmpty(source, state);
+        diff.clear();
+        state.buildDiff(diff);
+        assertEquals("Unexpected operations", 0, diff.size());
+    }
+
+    public void testTrimEmptyUntouched() {
+        final ContactsSource source = getSource();
+        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+        final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+        // Build "before" that has empty row
+        final EntityDelta state = getEntity();
+        final ContentValues before = new ContentValues();
+        before.put(Data._ID, TEST_ID);
+        before.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        state.addEntry(ValuesDelta.fromBefore(before));
+
+        // Build diff, expecting no changes
+        final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+        state.buildDiff(diff);
+        assertEquals("Unexpected operations", 0, diff.size());
+
+        // Try trimming existing empty, which we shouldn't touch
+        EntityModifier.trimEmpty(source, state);
+        diff.clear();
+        state.buildDiff(diff);
+        assertEquals("Unexpected operations", 0, diff.size());
+    }
+
+    public void testTrimEmptyAfterUpdate() {
+        final ContactsSource source = getSource();
+        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+        final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+        // Build "before" that has row with some phone number
+        final ContentValues before = new ContentValues();
+        before.put(Data._ID, TEST_ID);
+        before.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        before.put(kindPhone.typeColumn, typeHome.rawValue);
+        before.put(Phone.NUMBER, TEST_PHONE);
+        final EntityDelta state = getEntity(before);
+
+        // Build diff, expecting no changes
+        final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+        state.buildDiff(diff);
+        assertEquals("Unexpected operations", 0, diff.size());
+
+        // Now update row by changing number to empty string, expecting single update
+        final ValuesDelta child = state.getEntry(TEST_ID);
+        child.put(Phone.NUMBER, "");
+        diff.clear();
+        state.buildDiff(diff);
+        assertEquals("Unexpected operations", 3, diff.size());
+        {
+            final ContentProviderOperation oper = diff.get(0);
+            assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+        {
+            final ContentProviderOperation oper = diff.get(1);
+            assertEquals("Incorrect type", TYPE_UPDATE, oper.getType());
+            assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+        }
+        {
+            final ContentProviderOperation oper = diff.get(2);
+            assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+
+        // Now run trim, which should turn that update into delete
+        EntityModifier.trimEmpty(source, state);
+        diff.clear();
+        state.buildDiff(diff);
+        assertEquals("Unexpected operations", 3, diff.size());
+        {
+            final ContentProviderOperation oper = diff.get(0);
+            assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+        {
+            final ContentProviderOperation oper = diff.get(1);
+            assertEquals("Incorrect type", TYPE_DELETE, oper.getType());
+            assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+        }
+        {
+            final ContentProviderOperation oper = diff.get(2);
+            assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+    }
 }