Merge change 24628 into eclair

* changes:
  Fix an exception in Contacts when you select text backwards from the end.
diff --git a/res/layout-finger/set_primary_checkbox.xml b/res/layout-finger/set_primary_checkbox.xml
new file mode 100644
index 0000000..bba8cf9
--- /dev/null
+++ b/res/layout-finger/set_primary_checkbox.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    android:paddingLeft="14dip"
+    android:paddingRight="15dip"
+    android:orientation="vertical">
+    
+    <CheckBox
+        android:id="@+id/setPrimary"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:focusable="true"
+        android:clickable="true"
+        android:text="@string/make_primary"/>
+</LinearLayout>
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/res/values/strings.xml b/res/values/strings.xml
index 46a4210..5974dfc 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -100,10 +100,10 @@
     <string name="menu_deleteContact">Delete contact</string>
 
     <!-- Menu item used to call a specific contact when viewing the details of that contact. -->
-    <string name="menu_call">Call</string>
+    <string name="menu_call">Call contact</string>
 
     <!-- Menu item used to send an SMS or MMS message to a specific phone number or a contacts default phone number -->
-    <string name="menu_sendSMS">Send SMS/MMS</string>
+    <string name="menu_sendSMS">Text contact</string>
 
     <!-- Menu item used to send an email message to a specific email address -->
     <string name="menu_sendEmail">Send email</string>
@@ -822,6 +822,15 @@
 
     <!-- Label for onscreen "Dial" button -->
     <string name="dial_button_label">Dial</string>
+    
+    <!-- Title for the call disambiguation dialog -->
+    <string name="call_disambig_title">Call using</string>
+    
+    <!-- Title for the sms disambiguation dialog -->
+    <string name="sms_disambig_title">Text using</string>
+    
+    <!-- Message next to disamgiguation dialog check box -->
+    <string name="make_primary">Remember this choice</string>
 
     <!-- Shown as a toast when the user taps on a Fast-Track icon, and no application
          was found that could perform the selected action -->
diff --git a/src/com/android/contacts/ContactsListActivity.java b/src/com/android/contacts/ContactsListActivity.java
index d81e20c..7d76531 100644
--- a/src/com/android/contacts/ContactsListActivity.java
+++ b/src/com/android/contacts/ContactsListActivity.java
@@ -100,6 +100,7 @@
 
 import java.lang.ref.SoftReference;
 import java.lang.ref.WeakReference;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Locale;
 
@@ -310,6 +311,8 @@
 
     private String mShortcutAction;
 
+    private int mScrollState;
+
     /**
      * Internal query type when in mode {@link #MODE_QUERY_PICK_TO_VIEW}.
      */
@@ -628,6 +631,7 @@
     protected void onResume() {
         super.onResume();
 
+        mScrollState = OnScrollListener.SCROLL_STATE_IDLE;
         boolean runQuery = true;
         Activity parent = getParent();
 
@@ -914,27 +918,13 @@
         menu.add(0, MENU_ITEM_VIEW_CONTACT, 0, R.string.menu_viewContact)
                 .setIntent(new Intent(Intent.ACTION_VIEW, contactUri));
 
-        /*
-        // Calling contact
-        long phoneId = cursor.getLong(PRIMARY_PHONE_ID_COLUMN_INDEX);
-        if (phoneId > 0) {
-            // Get the display label for the number
-            CharSequence label = cursor.getString(PRIMARY_PHONE_LABEL_COLUMN_INDEX);
-            int type = cursor.getInt(PRIMARY_PHONE_TYPE_COLUMN_INDEX);
-            label = ContactsUtils.getDisplayLabel(
-                    this, CommonDataKinds.Phone.CONTENT_ITEM_TYPE, type, label);
-            Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
-                    ContentUris.withAppendedId(Data.CONTENT_URI, id));
+        if (cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0) {
+            // Calling contact
             menu.add(0, MENU_ITEM_CALL, 0,
-                    String.format(getString(R.string.menu_callNumber), label)).setIntent(intent);
-
+                    getString(R.string.menu_call));
             // Send SMS item
-            menu.add(0, MENU_ITEM_SEND_SMS, 0, R.string.menu_sendSMS)
-                    .setIntent(new Intent(Intent.ACTION_SENDTO,
-                            Uri.fromParts("sms",
-                                    cursor.getString(PRIMARY_PHONE_NUMBER_COLUMN_INDEX), null)));
+            menu.add(0, MENU_ITEM_SEND_SMS, 0, getString(R.string.menu_sendSMS));
         }
-         */
 
         // Star toggling
         int starState = cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX);
@@ -972,6 +962,16 @@
                 return true;
             }
 
+            case MENU_ITEM_CALL: {
+                callContact(cursor);
+                return true;
+            }
+
+            case MENU_ITEM_SEND_SMS: {
+                smsContact(cursor);
+                return true;
+            }
+
             case MENU_ITEM_DELETE: {
                 final Uri selectedUri = getContactUri(info.position);
                 doContactDelete(selectedUri);
@@ -1622,33 +1622,86 @@
         ListView list = getListView();
         if (list.hasFocus()) {
             Cursor cursor = (Cursor) list.getSelectedItem();
-            if (cursor != null) {
-                boolean hasPhone = cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0;
-                if (!hasPhone) {
-                    // There is no phone number.
-                    signalError();
-                    return false;
-                }
+            return callContact(cursor);
+        }
+        return false;
+    }
 
-                // TODO: transition to use lookup instead of strong id
-                final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
-                final String phone = ContactsUtils.querySuperPrimaryPhone(getContentResolver(),
-                        contactId);
-                if (phone == null) {
-                    signalError();
-                    return false;
-                }
+    boolean callContact(Cursor cursor) {
+        return callOrSmsContact(cursor, false /*call*/);
+    }
 
-                Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
-                        Uri.fromParts(Constants.SCHEME_TEL, phone, null));
-                startActivity(intent);
-                return true;
+    boolean smsContact(Cursor cursor) {
+        return callOrSmsContact(cursor, true /*sms*/);
+    }
+
+    /**
+     * Calls the contact which the cursor is point to.
+     * @return true if the call was initiated, false otherwise
+     */
+    boolean callOrSmsContact(Cursor cursor, boolean sendSms) {
+        if (cursor != null) {
+            boolean hasPhone = cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0;
+            if (!hasPhone) {
+                // There is no phone number.
+                signalError();
+                return false;
             }
+
+            String phone = null;
+            Cursor phonesCursor = null;
+            phonesCursor = queryPhoneNumbers(cursor.getLong(SUMMARY_ID_COLUMN_INDEX));
+            if (phonesCursor == null || phonesCursor.getCount() == 0) {
+                // No valid number
+                signalError();
+                return false;
+            } else if (phonesCursor.getCount() == 1) {
+                // only one number, call it.
+                phone = phonesCursor.getString(phonesCursor.getColumnIndex(Phone.NUMBER));
+            } else {
+                phonesCursor.moveToPosition(-1);
+                while (phonesCursor.moveToNext()) {
+                    if (phonesCursor.getInt(phonesCursor.
+                            getColumnIndex(Data.IS_SUPER_PRIMARY)) != 0) {
+                        // Found super primary, call it.
+                        phone = phonesCursor.
+                        getString(phonesCursor.getColumnIndex(Phone.NUMBER));
+                        break;
+                    }
+                }
+            }
+
+            if (phone == null) {
+                // Display dialog to choose a number to call.
+                PhoneDisambigDialog phoneDialog = new PhoneDisambigDialog(
+                        this, phonesCursor, sendSms);
+                phoneDialog.show();
+            } else {
+                if (sendSms) {
+                    ContactsUtils.initiateSms(this, phone);
+                } else {
+                    ContactsUtils.initiateCall(this, phone);
+                }
+            }
+            return true;
         }
 
         return false;
     }
 
+    private Cursor queryPhoneNumbers(long contactId) {
+        Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+        Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY);
+
+        Cursor c = getContentResolver().query(dataUri,
+                new String[] {Data._ID, Phone.NUMBER, Data.IS_SUPER_PRIMARY},
+                Data.MIMETYPE + "=?", new String[] {Phone.CONTENT_ITEM_TYPE}, null);
+        if (c != null && c.moveToFirst()) {
+            return c;
+        }
+        return null;
+    }
+
     /**
      * Signal an error to the user.
      */
@@ -1758,7 +1811,7 @@
         private CharSequence[] mLocalizedLabels;
         private boolean mDisplayPhotos = false;
         private boolean mDisplayAdditionalData = true;
-        private SparseArray<SoftReference<Bitmap>> mBitmapCache = null;
+        private HashMap<Long, SoftReference<Bitmap>> mBitmapCache = null;
         private HashSet<ImageView> mItemsMissingImages = null;
         private int mFrequentSeparatorPos = ListView.INVALID_POSITION;
         private boolean mDisplaySectionHeaders = true;
@@ -1766,7 +1819,6 @@
         private Cursor mSuggestionsCursor;
         private int mSuggestionsCursorCount;
         private ImageFetchHandler mHandler;
-        private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE;
         private static final int FETCH_IMAGE_MSG = 1;
 
         public ContactItemListAdapter(Context context) {
@@ -1810,7 +1862,7 @@
             if ((mMode & MODE_MASK_SHOW_PHOTOS) == MODE_MASK_SHOW_PHOTOS) {
                 mDisplayPhotos = true;
                 setViewResource(R.layout.contacts_list_item_photo);
-                mBitmapCache = new SparseArray<SoftReference<Bitmap>>();
+                mBitmapCache = new HashMap<Long, SoftReference<Bitmap>>();
                 mItemsMissingImages = new HashSet<ImageView>();
             }
 
@@ -1827,30 +1879,37 @@
                     return;
                 }
                 switch(message.what) {
-                    case FETCH_IMAGE_MSG:
+                    case FETCH_IMAGE_MSG: {
                         ImageView imageView = (ImageView) message.obj;
-                        int pos = (Integer) imageView.getTag();
-                        Cursor cursor = (Cursor) getItem(pos);
+                        long photoId = (Long) imageView.getTag();
+                        if (photoId == 0) {
+                            break;
+                        }
 
-                        if (cursor != null && !cursor.isNull(SUMMARY_PHOTO_ID_COLUMN_INDEX)) {
-                            try {
-                                Bitmap photo = ContactsUtils.loadContactPhoto(
-                                        mContext, cursor.getInt(SUMMARY_PHOTO_ID_COLUMN_INDEX),
-                                        null);
-                                mBitmapCache.put(pos, new SoftReference<Bitmap>(photo));
-                                if (photo != null) {
-                                    imageView.setImageBitmap(photo);
-                                }
-                            } catch (OutOfMemoryError e) {
-                                // Not enough memory for the photo, do nothing.
+                        Bitmap photo = null;
+                        try {
+                            photo = ContactsUtils.loadContactPhoto(mContext, photoId, null);
+                        } catch (OutOfMemoryError e) {
+                            // Not enough memory for the photo, do nothing.
+                        }
+
+                        if (photo == null) {
+                            break;
+                        }
+
+                        mBitmapCache.put(photoId, new SoftReference<Bitmap>(photo));
+
+                        // Make sure the photoId on this image view has not changed
+                        // while we were loading the image.
+                        synchronized (imageView) {
+                            long currentPhotoId = (Long) imageView.getTag();
+                            if (currentPhotoId == photoId) {
+                                imageView.setImageBitmap(photo);
+                                mItemsMissingImages.remove(imageView);
                             }
                         }
-
-                        if (imageView.getDrawable() == null) {
-                            imageView.setImageResource(R.drawable.ic_contact_list_picture);
-                        }
-                        mItemsMissingImages.remove(imageView);
                         break;
+                    }
                 }
             }
 
@@ -2052,29 +2111,44 @@
 
             // Set the photo, if requested
             if (mDisplayPhotos) {
-                int pos = cursor.getPosition();
-                Bitmap photo = null;
-                cache.photoView.setImageBitmap(null);
-                cache.photoView.setTag(pos);
 
-                // Look for the cached bitmap
-                SoftReference<Bitmap> ref = mBitmapCache.get(pos);
-                if (ref != null) {
-                    photo = ref.get();
+                long photoId = 0;
+                if (!cursor.isNull(SUMMARY_PHOTO_ID_COLUMN_INDEX)) {
+                    photoId = cursor.getLong(SUMMARY_PHOTO_ID_COLUMN_INDEX);
                 }
 
-                // Bind the photo, or use the fallback no photo resource
-                if (photo != null) {
-                    cache.photoView.setImageBitmap(photo);
-                } else {
-                    // Cache miss
+                cache.photoView.setTag(photoId);
+
+                if (photoId == 0) {
                     cache.photoView.setImageResource(R.drawable.ic_contact_list_picture);
-                    if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
-                        // Scrolling is idle, go get the image right now.
-                        sendFetchImageMessage(cache.photoView);
+                } else {
+
+                    Bitmap photo = null;
+
+                    // Look for the cached bitmap
+                    SoftReference<Bitmap> ref = mBitmapCache.get(photoId);
+                    if (ref != null) {
+                        photo = ref.get();
+                        if (photo == null) {
+                            mBitmapCache.remove(photoId);
+                        }
+                    }
+
+                    // Bind the photo, or use the fallback no photo resource
+                    if (photo != null) {
+                        cache.photoView.setImageBitmap(photo);
                     } else {
-                        // Add it to a set of images that will be populated when scrolling stops.
+                        // Cache miss
+                        cache.photoView.setImageResource(R.drawable.ic_contact_list_picture);
+
+                        // Add it to a set of images that are populated asynchronously.
                         mItemsMissingImages.add(cache.photoView);
+
+                        if (mScrollState != OnScrollListener.SCROLL_STATE_FLING) {
+
+                            // Scrolling is idle or slow, go get the image right now.
+                            sendFetchImageMessage(cache.photoView);
+                        }
                     }
                 }
             }
@@ -2361,8 +2435,8 @@
 
         public void onScrollStateChanged(AbsListView view, int scrollState) {
             mScrollState = scrollState;
-            if (scrollState != OnScrollListener.SCROLL_STATE_IDLE) {
-                // If we are not idle, stop loading images.
+            if (scrollState == OnScrollListener.SCROLL_STATE_FLING) {
+                // If we are in a fling, stop loading images.
                 clearImageFetching();
             } else if (mDisplayPhotos) {
                 processMissingImageItems(view);
@@ -2371,7 +2445,6 @@
 
         private void processMissingImageItems(AbsListView view) {
             for (ImageView iv : mItemsMissingImages) {
-                int pos = (Integer) iv.getTag();
                 sendFetchImageMessage(iv);
             }
         }
diff --git a/src/com/android/contacts/ContactsUtils.java b/src/com/android/contacts/ContactsUtils.java
index 7537d30..dea0bad 100644
--- a/src/com/android/contacts/ContactsUtils.java
+++ b/src/com/android/contacts/ContactsUtils.java
@@ -353,4 +353,22 @@
         }
         return createTabIndicatorView(parent, null, icon);
     }
+
+    /**
+     * Kick off an intent to initiate a call.
+     */
+    public static void initiateCall(Context context, CharSequence phoneNumber) {
+        Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
+                Uri.fromParts("tel", phoneNumber.toString(), null));
+        context.startActivity(intent);
+    }
+
+    /**
+     * Kick off an intent to initiate an Sms/Mms message.
+     */
+    public static void initiateSms(Context context, CharSequence phoneNumber) {
+        Intent intent = new Intent(Intent.ACTION_SENDTO,
+                Uri.fromParts("sms", phoneNumber.toString(), null));
+        context.startActivity(intent);
+    }
 }
diff --git a/src/com/android/contacts/PhoneDisambigDialog.java b/src/com/android/contacts/PhoneDisambigDialog.java
new file mode 100644
index 0000000..58d3721
--- /dev/null
+++ b/src/com/android/contacts/PhoneDisambigDialog.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2009 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;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+
+import android.app.AlertDialog;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.database.Cursor;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.SimpleCursorAdapter;
+
+/**
+ * Class used for displaying a dialog with a list of phone numbers of which
+ * one will be chosen to make a call or initiate an sms message.
+ */
+public class PhoneDisambigDialog implements DialogInterface.OnClickListener,
+        DialogInterface.OnDismissListener, CompoundButton.OnCheckedChangeListener{
+
+    private boolean mMakePrimary = false;
+    private Context mContext;
+    private AlertDialog mDialog;
+    private boolean mSendSms;
+    private Cursor mPhonesCursor;
+
+    public PhoneDisambigDialog(Context context, Cursor phonesCursor) {
+        this(context, phonesCursor, false /*make call*/);
+    }
+
+    public PhoneDisambigDialog(Context context, Cursor phonesCursor, boolean sendSms) {
+        mContext = context;
+        mSendSms = sendSms;
+        mPhonesCursor = phonesCursor;
+
+        LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+        View setPrimaryView = inflater.
+                inflate(R.layout.set_primary_checkbox, null);
+        ((CheckBox) setPrimaryView.findViewById(R.id.setPrimary)).
+                setOnCheckedChangeListener(this);
+
+        // Need to show disambig dialogue.
+        AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mContext).
+            setCursor(mPhonesCursor, this, Phone.NUMBER).
+                    setTitle(sendSms ? R.string.sms_disambig_title : R.string.call_disambig_title).
+                    setView(setPrimaryView);
+
+        mDialog = dialogBuilder.create();
+    }
+
+    /**
+     * Show the dialog.
+     */
+    public void show() {
+        mDialog.show();
+    }
+
+    public void onClick(DialogInterface dialog, int which) {
+        if (mPhonesCursor.moveToPosition(which)) {
+            long id = mPhonesCursor.getLong(mPhonesCursor.getColumnIndex(Data._ID));
+            String phone = mPhonesCursor.getString(mPhonesCursor.getColumnIndex(Phone.NUMBER));
+            if (mMakePrimary) {
+                ContentValues values = new ContentValues(1);
+                values.put(Data.IS_SUPER_PRIMARY, 1);
+                mContext.getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, id),
+                        values, null, null);
+            }
+
+            if (mSendSms) {
+                ContactsUtils.initiateSms(mContext, phone);
+            } else {
+                ContactsUtils.initiateCall(mContext, phone);
+            }
+        } else {
+            dialog.dismiss();
+        }
+    }
+
+    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+        mMakePrimary = isChecked;
+    }
+
+    public void onDismiss(DialogInterface dialog) {
+        mPhonesCursor.close();
+    }
+}
diff --git a/src/com/android/contacts/ViewContactActivity.java b/src/com/android/contacts/ViewContactActivity.java
index ddd0abb..d2328ee 100644
--- a/src/com/android/contacts/ViewContactActivity.java
+++ b/src/com/android/contacts/ViewContactActivity.java
@@ -614,17 +614,7 @@
         }
 
         // Update the primary values in the data record.
-        ContentValues values = new ContentValues(2);
-        values.put(Data.IS_PRIMARY, 1);
-
-        if (entry.ids.size() > 0) {
-            for (int i = 0; i < entry.ids.size(); i++) {
-                getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI,
-                        entry.ids.get(i)),
-                        values, null, null);
-            }
-        }
-
+        ContentValues values = new ContentValues(1);
         values.put(Data.IS_SUPER_PRIMARY, 1);
         getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, entry.id),
                 values, null, null);
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 885a06b..fdac44c 100644
--- a/src/com/android/contacts/model/HardCodedSources.java
+++ b/src/com/android/contacts/model/HardCodedSources.java
@@ -28,7 +28,6 @@
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
-import android.provider.ContactsContract;
 import android.provider.ContactsContract.Groups;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
@@ -44,8 +43,6 @@
 import android.provider.ContactsContract.Contacts.Data;
 import android.view.inputmethod.EditorInfo;
 
-import java.util.ArrayList;
-
 /**
  * Hard-coded definition of some {@link ContactsSource} constraints, since the
  * XML language hasn't been finalized.
@@ -161,6 +158,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 +446,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);
         }
 
@@ -556,6 +561,39 @@
         }
 
         {
+            // EXCHANGE: POSTAL
+            DataKind kind = new DataKind(StructuredPostal.CONTENT_ITEM_TYPE,
+                    R.string.postalLabelsGroup, R.drawable.sym_action_map, 25, true);
+
+            kind.actionHeader = new ActionInflater(list.resPackageName, kind);
+            // TODO: build body from various structured fields
+            kind.actionBody = new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS);
+
+            kind.typeColumn = StructuredPostal.TYPE;
+            kind.typeList = Lists.newArrayList();
+            kind.typeList.add(new EditType(StructuredPostal.TYPE_WORK, R.string.type_work,
+                    R.string.map_work).setSpecificMax(1));
+            kind.typeList.add(new EditType(StructuredPostal.TYPE_HOME, R.string.type_home,
+                    R.string.map_home).setSpecificMax(1));
+            kind.typeList.add(new EditType(StructuredPostal.TYPE_OTHER, R.string.type_other,
+                    R.string.map_other).setSpecificMax(1));
+
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(StructuredPostal.STREET, R.string.postal_street,
+                    FLAGS_POSTAL));
+            kind.fieldList.add(new EditField(StructuredPostal.CITY, R.string.postal_city,
+                    FLAGS_POSTAL));
+            kind.fieldList.add(new EditField(StructuredPostal.REGION, R.string.postal_region,
+                    FLAGS_POSTAL));
+            kind.fieldList.add(new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode,
+                    FLAGS_POSTAL));
+            kind.fieldList.add(new EditField(StructuredPostal.COUNTRY, R.string.postal_country,
+                    FLAGS_POSTAL, true));
+
+            list.add(kind);
+        }
+
+        {
             // EXCHANGE: NICKNAME
             DataKind kind = new DataKind(Nickname.CONTENT_ITEM_TYPE,
                     R.string.nicknameLabelsGroup, -1, 115, true);
@@ -633,11 +671,53 @@
      * Hard-coded instance of {@link ContactsSource} for Facebook.
      */
     static void buildFacebook(Context context, ContactsSource list) {
-        // Rely on the fallback source for now, it has a generic set of sources
-        buildFallback(context, list);
-
         list.accountType = ACCOUNT_TYPE_FACEBOOK;
         list.readOnly = true;
+
+        {
+            // FACEBOOK: PHONE
+            DataKind kind = new DataKind(Phone.CONTENT_ITEM_TYPE,
+                    R.string.phoneLabelsGroup, android.R.drawable.sym_action_call, 10, true);
+            kind.iconAltRes = R.drawable.sym_action_sms;
+
+            kind.actionHeader = new ActionInflater(list.resPackageName, kind);
+            kind.actionAltHeader = new ActionAltInflater(list.resPackageName, kind);
+            kind.actionBody = new SimpleInflater(Phone.NUMBER);
+
+            kind.typeColumn = Phone.TYPE;
+            kind.typeList = Lists.newArrayList();
+            kind.typeList.add(new EditType(Phone.TYPE_MOBILE, R.string.type_mobile,
+                    R.string.call_mobile, R.string.sms_mobile));
+            kind.typeList.add(new EditType(Phone.TYPE_OTHER, R.string.type_other,
+                    R.string.call_other, R.string.sms_other));
+
+            list.add(kind);
+        }
+
+        {
+            // FACEBOOK: EMAIL
+            DataKind kind = new DataKind(Email.CONTENT_ITEM_TYPE,
+                    R.string.emailLabelsGroup, android.R.drawable.sym_action_email, 15, true);
+
+            kind.actionHeader = new ActionInflater(list.resPackageName, kind);
+            kind.actionBody = new SimpleInflater(Email.DATA);
+
+            kind.typeColumn = Email.TYPE;
+            kind.typeList = Lists.newArrayList();
+            kind.typeList
+                    .add(new EditType(Email.TYPE_HOME, R.string.type_home, R.string.email_home));
+            kind.typeList
+                    .add(new EditType(Email.TYPE_WORK, R.string.type_work, R.string.email_work));
+            kind.typeList.add(new EditType(Email.TYPE_OTHER, R.string.type_other,
+                    R.string.email_other));
+            kind.typeList.add(new EditType(Email.TYPE_CUSTOM, R.string.type_custom,
+                    R.string.email_home).setSecondary(true).setCustomColumn(Email.LABEL));
+
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+            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());
+        }
+    }
 }