Merge "Allow adding/replacing a photo from contact card."
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 11b9cde..48fe520 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -544,7 +544,13 @@
                 <data android:mimeType="image/*" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
-            />
+        </activity>
+
+        <!-- Internal photo selection activity -->
+        <activity android:name=".activities.PhotoSelectionActivity"
+            android:theme="@style/Theme.PhotoSelector"
+            android:launchMode="singleTop"
+            android:windowSoftInputMode="stateUnchanged">
         </activity>
 
         <!-- Interstitial activity that shows a phone disambig dialog -->
diff --git a/res/layout-sw580dp/detail_header_contact_without_updates.xml b/res/layout-sw580dp/detail_header_contact_without_updates.xml
index 9261f11..0f0e3c2 100644
--- a/res/layout-sw580dp/detail_header_contact_without_updates.xml
+++ b/res/layout-sw580dp/detail_header_contact_without_updates.xml
@@ -40,9 +40,7 @@
             android:layout_height="match_parent"
             android:orientation="horizontal">
 
-            <ImageView
-                android:id="@+id/photo"
-                android:scaleType="centerCrop"
+            <include layout="@layout/photo_selector_view"
                 android:layout_width="0dip"
                 android:layout_height="match_parent"
                 android:layout_weight="2" />
diff --git a/res/layout-sw680dp-w1000dp/contact_detail_fragment.xml b/res/layout-sw680dp-w1000dp/contact_detail_fragment.xml
index ecf8130..f70c770 100644
--- a/res/layout-sw680dp-w1000dp/contact_detail_fragment.xml
+++ b/res/layout-sw680dp-w1000dp/contact_detail_fragment.xml
@@ -38,12 +38,12 @@
         android:layout_width="match_parent"
         android:layout_height="0dip">
 
-        <ImageView android:id="@+id/photo"
-            android:scaleType="centerCrop"
+        <include android:id="@+id/static_photo_container"
+            layout="@layout/photo_selector_view"
             android:layout_width="@dimen/detail_contact_photo_size"
             android:layout_height="@dimen/detail_contact_photo_size"
             android:layout_marginTop="@dimen/detail_contact_photo_margin"
-            android:layout_marginRight="@dimen/detail_contact_photo_margin"/>
+            android:layout_marginRight="@dimen/detail_contact_photo_margin" />
 
         <ListView android:id="@android:id/list"
             android:layout_width="0dip"
diff --git a/res/layout-sw680dp-w1000dp/detail_header_contact_with_updates.xml b/res/layout-sw680dp-w1000dp/detail_header_contact_with_updates.xml
index dfba659..7758a21 100644
--- a/res/layout-sw680dp-w1000dp/detail_header_contact_with_updates.xml
+++ b/res/layout-sw680dp-w1000dp/detail_header_contact_with_updates.xml
@@ -27,9 +27,7 @@
     android:paddingBottom="8dip"
     android:orientation="horizontal">
 
-    <ImageView
-        android:id="@+id/photo"
-        android:scaleType="centerCrop"
+    <include layout="@layout/photo_selector_view"
         android:layout_width="@dimen/detail_contact_photo_size"
         android:layout_height="@dimen/detail_contact_photo_size" />
 
diff --git a/res/layout-w470dp/contact_detail_fragment.xml b/res/layout-w470dp/contact_detail_fragment.xml
index 415bb56..166610e 100644
--- a/res/layout-w470dp/contact_detail_fragment.xml
+++ b/res/layout-w470dp/contact_detail_fragment.xml
@@ -37,12 +37,12 @@
         android:layout_above="@id/contact_quick_fix"
         android:layout_height="match_parent" >
 
-        <ImageView android:id="@+id/photo"
-            android:scaleType="centerCrop"
+        <include android:id="@+id/static_photo_container"
+            layout="@layout/photo_selector_view"
             android:layout_width="128dip"
             android:layout_height="128dip"
             android:layout_marginLeft="@dimen/detail_contact_photo_margin"
-            android:layout_marginTop="@dimen/detail_contact_photo_margin"/>
+            android:layout_marginTop="@dimen/detail_contact_photo_margin" />
 
         <ListView android:id="@android:id/list"
             android:layout_width="0dip"
diff --git a/res/layout/detail_header_contact_without_updates.xml b/res/layout/detail_header_contact_without_updates.xml
index 2de7711..6422445 100644
--- a/res/layout/detail_header_contact_without_updates.xml
+++ b/res/layout/detail_header_contact_without_updates.xml
@@ -26,9 +26,7 @@
     android:layout_height="wrap_content"
     ex:ratio="0.5"
     ex:direction="widthToHeight">
-    <ImageView
-        android:id="@+id/photo"
-        android:scaleType="centerCrop"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent" />
+
+    <include layout="@layout/photo_selector_view" />
+
 </view>
\ No newline at end of file
diff --git a/res/layout/photo_selector_view.xml b/res/layout/photo_selector_view.xml
new file mode 100644
index 0000000..0006559
--- /dev/null
+++ b/res/layout/photo_selector_view.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<!--
+  View for displaying photos that show a photo selector when clicked.
+-->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <ImageView
+        android:id="@+id/photo"
+        android:scaleType="centerCrop"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+    <View
+        android:id="@+id/photo_touch_intercept_overlay"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="?android:attr/selectableItemBackground"
+        android:visibility="gone" />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/res/layout/photoselection_activity.xml b/res/layout/photoselection_activity.xml
new file mode 100644
index 0000000..75f729b
--- /dev/null
+++ b/res/layout/photoselection_activity.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:ex="http://schemas.android.com/apk/res/com.android.contacts"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <View
+        android:id="@+id/backdrop"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="#000000" />
+    <view
+        android:id="@+id/photo"
+        class="com.android.contacts.detail.TransformableImageView"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:scaleType="centerCrop" />
+</FrameLayout>
\ No newline at end of file
diff --git a/res/values/styles.xml b/res/values/styles.xml
index ece881e..9434104 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -314,6 +314,15 @@
         <item name="android:background">@color/quickcontact_tab_indicator</item>
     </style>
 
+    <style name="Theme.PhotoSelector" parent="@android:style/Theme.Holo.Light">
+        <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:windowFrame">@null</item>
+        <item name="android:windowContentOverlay">@null</item>
+        <item name="android:windowAnimationStyle">@null</item>
+        <item name="android:backgroundDimEnabled">false</item>
+        <item name="android:windowIsTranslucent">true</item>
+        <item name="android:windowNoTitle">true</item>
+    </style>
 
     <style name="GroupMembershipSizeTextAppearance" parent="@android:style/TextAppearance.Small"/>
 
diff --git a/src/com/android/contacts/ContactLoader.java b/src/com/android/contacts/ContactLoader.java
index c711b6c..1cba29b 100644
--- a/src/com/android/contacts/ContactLoader.java
+++ b/src/com/android/contacts/ContactLoader.java
@@ -301,8 +301,10 @@
             return mRequestedUri;
         }
 
-        @VisibleForTesting
-        /*package*/ long getId() {
+        /**
+         * Returns the contact ID.
+         */
+        public long getId() {
             return mId;
         }
 
diff --git a/src/com/android/contacts/activities/AttachPhotoActivity.java b/src/com/android/contacts/activities/AttachPhotoActivity.java
index a697c29..8d4cb5d 100644
--- a/src/com/android/contacts/activities/AttachPhotoActivity.java
+++ b/src/com/android/contacts/activities/AttachPhotoActivity.java
@@ -156,7 +156,7 @@
                 Bitmap photo = extras.getParcelable("data");
                 if (photo != null) {
                     ByteArrayOutputStream stream = new ByteArrayOutputStream();
-                    photo.compress(Bitmap.CompressFormat.JPEG, 75, stream);
+                    photo.compress(Bitmap.CompressFormat.PNG, 100, stream);
 
                     final ContentValues imageValues = new ContentValues();
                     imageValues.put(Photo.PHOTO, stream.toByteArray());
diff --git a/src/com/android/contacts/activities/ContactDetailActivity.java b/src/com/android/contacts/activities/ContactDetailActivity.java
index b949176..b353a0b 100644
--- a/src/com/android/contacts/activities/ContactDetailActivity.java
+++ b/src/com/android/contacts/activities/ContactDetailActivity.java
@@ -74,7 +74,7 @@
     private Handler mHandler = new Handler();
 
     @Override
-    public void onCreate(Bundle savedState) {
+    protected void onCreate(Bundle savedState) {
         super.onCreate(savedState);
         if (PhoneCapabilityTester.isUsingTwoPanes(this)) {
             // This activity must not be shown. We have to select the contact in the
diff --git a/src/com/android/contacts/activities/PhotoSelectionActivity.java b/src/com/android/contacts/activities/PhotoSelectionActivity.java
new file mode 100644
index 0000000..73a85eb
--- /dev/null
+++ b/src/com/android/contacts/activities/PhotoSelectionActivity.java
@@ -0,0 +1,474 @@
+/*
+ * Copyright (C) 2011 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.activities;
+
+import com.android.contacts.ContactSaveService;
+import com.android.contacts.R;
+import com.android.contacts.detail.PhotoSelectionHandler;
+import com.android.contacts.editor.PhotoActionPopup;
+import com.android.contacts.model.EntityDeltaList;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.view.View;
+import android.view.ViewGroup.MarginLayoutParams;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.widget.FrameLayout.LayoutParams;
+import android.widget.ImageView;
+
+import java.io.File;
+
+/**
+ * Popup activity for choosing a contact photo within the Contacts app.
+ */
+public class PhotoSelectionActivity extends Activity {
+
+    /** Number of ms for the animation to expand the photo. */
+    private static final int PHOTO_EXPAND_DURATION = 100;
+
+    /** Number of ms for the animation to contract the photo on activity exit. */
+    private static final int PHOTO_CONTRACT_DURATION = 50;
+
+    /** Number of ms for the animation to hide the backdrop on finish. */
+    private static final int BACKDROP_FADEOUT_DURATION = 100;
+
+    private static final String KEY_CURRENT_PHOTO_FILE = "currentphotofile";
+
+    private static final String KEY_SUB_ACTIVITY_IN_PROGRESS = "subinprogress";
+
+    /** Intent extra to get the photo bitmap. */
+    public static final String PHOTO_BITMAP = "photo_bitmap";
+
+    /** Intent extra to get the entity delta list. */
+    public static final String ENTITY_DELTA_LIST = "entity_delta_list";
+
+    /** Intent extra to indicate whether the contact is the user's profile. */
+    public static final String IS_PROFILE = "is_profile";
+
+    /** Intent extra to indicate whether the contact is from a directory (non-editable). */
+    public static final String IS_DIRECTORY_CONTACT = "is_directory_contact";
+
+    /**
+     * Intent extra to indicate whether the photo should be animated to show the full contents of
+     * the photo (on a larger portion of the screen) when clicked.  If unspecified or false, the
+     * photo will not move from its original location.
+     */
+    public static final String EXPAND_PHOTO = "expand_photo";
+
+    /** Source bounds of the image that was clicked on. */
+    private Rect mSourceBounds;
+
+    /** The photo bitmap. */
+    private Bitmap mPhotoBitmap;
+
+    /** Entity delta list of the contact. */
+    private EntityDeltaList mState;
+
+    /** Whether the contact is the user's profile. */
+    private boolean mIsProfile;
+
+    /** Whether the contact is from a directory. */
+    private boolean mIsDirectoryContact;
+
+    /** Whether to animate the photo to an expanded view covering more of the screen. */
+    private boolean mExpandPhoto;
+
+    /** The semi-transparent backdrop. */
+    private View mBackdrop;
+
+    /** The photo view. */
+    private ImageView mPhotoView;
+
+    /** The photo handler attached to this activity, if any. */
+    private PhotoHandler mPhotoHandler;
+
+    /** Animator to expand the photo out to full size. */
+    private ObjectAnimator mPhotoAnimator;
+
+    /** Listener for the animation. */
+    private AnimatorListenerAdapter mAnimationListener;
+
+    /** Whether a change in layout of the photo has occurred that has no animation yet. */
+    private boolean mAnimationPending;
+
+    /** Prior position of the image (for animating). */
+    Rect mOriginalPos = new Rect();
+
+    /** Layout params for the photo view before we started animating. */
+    private LayoutParams mPhotoStartParams;
+
+    /** Layout params for the photo view after we finished animating. */
+    private LayoutParams mPhotoEndParams;
+
+    /** Whether a sub-activity is currently in progress. */
+    private boolean mSubActivityInProgress;
+
+    /**
+     * A photo result received by the activity, persisted across activity lifecycle.
+     */
+    private PendingPhotoResult mPendingPhotoResult;
+
+    /**
+     * The photo file being interacted with, if any.  Saved/restored between activity instances.
+     */
+    private File mCurrentPhotoFile;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.photoselection_activity);
+        if (savedInstanceState != null) {
+            String fileName = savedInstanceState.getString(KEY_CURRENT_PHOTO_FILE);
+            if (fileName != null) {
+                mCurrentPhotoFile = new File(fileName);
+            }
+            mSubActivityInProgress = savedInstanceState.getBoolean(KEY_SUB_ACTIVITY_IN_PROGRESS);
+        }
+
+        // Pull data out of the intent.
+        final Intent intent = getIntent();
+        mPhotoBitmap = intent.getParcelableExtra(PHOTO_BITMAP);
+        mState = (EntityDeltaList) intent.getParcelableExtra(ENTITY_DELTA_LIST);
+        mIsProfile = intent.getBooleanExtra(IS_PROFILE, false);
+        mIsDirectoryContact = intent.getBooleanExtra(IS_DIRECTORY_CONTACT, false);
+        mExpandPhoto = intent.getBooleanExtra(EXPAND_PHOTO, false);
+
+        mBackdrop = findViewById(R.id.backdrop);
+        mPhotoView = (ImageView) findViewById(R.id.photo);
+        mSourceBounds = intent.getSourceBounds();
+
+        // Fade in the background.
+        animateInBackground();
+
+        // Dismiss the dialog on clicking the backdrop.
+        mBackdrop.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                finish();
+            }
+        });
+
+        // Wait until the layout pass to show the photo, so that the source bounds will match up.
+        OnGlobalLayoutListener globalLayoutListener = new OnGlobalLayoutListener() {
+            @Override
+            public void onGlobalLayout() {
+                displayPhoto();
+                mBackdrop.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+            }
+        };
+        mBackdrop.getViewTreeObserver().addOnGlobalLayoutListener(globalLayoutListener);
+    }
+
+    @Override
+    public void finish() {
+        if (!mSubActivityInProgress) {
+            closePhotoAndFinish();
+        } else {
+            activityFinish();
+        }
+    }
+
+    /**
+     * Builds a well-formed intent for invoking this activity.
+     * @param context The context.
+     * @param photoBitmap The bitmap of the current photo.
+     * @param photoBounds The pixel bounds of the current photo.
+     * @param delta The entity delta list for the contact.
+     * @param isProfile Whether the contact is the user's profile.
+     * @param isDirectoryContact Whether the contact comes from a directory (non-editable).
+     * @param expandPhotoOnClick Whether the photo should be expanded on click or not (generally,
+     *     this should be true for phones, and false for tablets).
+     * @return An intent that can be used to invoke the photo selection activity.
+     */
+    public static Intent buildIntent(Context context, Bitmap photoBitmap, Rect photoBounds,
+            EntityDeltaList delta, boolean isProfile, boolean isDirectoryContact,
+            boolean expandPhotoOnClick) {
+        Intent intent = new Intent(context, PhotoSelectionActivity.class);
+        intent.putExtra(PHOTO_BITMAP, photoBitmap);
+        intent.setSourceBounds(photoBounds);
+        intent.putExtra(ENTITY_DELTA_LIST, (Parcelable) delta);
+        intent.putExtra(IS_PROFILE, isProfile);
+        intent.putExtra(IS_DIRECTORY_CONTACT, isDirectoryContact);
+        intent.putExtra(EXPAND_PHOTO, expandPhotoOnClick);
+        return intent;
+    }
+
+    private void activityFinish() {
+        super.finish();
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        if (mPhotoAnimator != null) {
+            mPhotoAnimator.cancel();
+            mPhotoAnimator = null;
+        }
+        if (mPhotoHandler != null) {
+            mPhotoHandler.destroy();
+            mPhotoHandler = null;
+        }
+    }
+
+    private void displayPhoto() {
+        if (mPhotoBitmap != null) {
+            final int[] pos = new int[2];
+            mBackdrop.getLocationOnScreen(pos);
+            LayoutParams layoutParams = new LayoutParams(mSourceBounds.width(),
+                    mSourceBounds.height());
+            mOriginalPos.left = mSourceBounds.left - pos[0];
+            mOriginalPos.top = mSourceBounds.top - pos[1];
+            mOriginalPos.right = mOriginalPos.left + mSourceBounds.width();
+            mOriginalPos.bottom = mOriginalPos.top + mSourceBounds.height();
+            layoutParams.setMargins(mOriginalPos.left, mOriginalPos.top, mOriginalPos.right,
+                    mOriginalPos.bottom);
+            mPhotoStartParams = layoutParams;
+            mPhotoView.setLayoutParams(layoutParams);
+            mPhotoView.requestLayout();
+
+            mPhotoView.setImageBitmap(mPhotoBitmap);
+            mPhotoView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
+                @Override
+                public void onLayoutChange(View v, int left, int top, int right, int bottom,
+                        int oldLeft, int oldTop, int oldRight, int oldBottom) {
+                    if (mAnimationPending) {
+                        mAnimationPending = false;
+                        PropertyValuesHolder pvhLeft =
+                                PropertyValuesHolder.ofInt("left", mOriginalPos.left, left);
+                        PropertyValuesHolder pvhTop =
+                                PropertyValuesHolder.ofInt("top", mOriginalPos.top, top);
+                        PropertyValuesHolder pvhRight =
+                                PropertyValuesHolder.ofInt("right", mOriginalPos.right, right);
+                        PropertyValuesHolder pvhBottom =
+                                PropertyValuesHolder.ofInt("bottom", mOriginalPos.bottom, bottom);
+                        ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(mPhotoView,
+                                pvhLeft, pvhTop, pvhRight, pvhBottom).setDuration(
+                                PHOTO_EXPAND_DURATION);
+                        if (mAnimationListener != null) {
+                            anim.addListener(mAnimationListener);
+                        }
+                        anim.start();
+                    }
+                }
+            });
+            attachPhotoHandler();
+        }
+    }
+
+    private LayoutParams getPhotoEndParams() {
+        if (mPhotoEndParams == null) {
+            mPhotoEndParams = new LayoutParams(mPhotoStartParams);
+            if (mExpandPhoto) {
+                Rect bounds = new Rect();
+                mBackdrop.getDrawingRect(bounds);
+                if (bounds.height() > bounds.width()) {
+                    //Take up full width.
+                    mPhotoEndParams.width = bounds.width();
+                    mPhotoEndParams.height = bounds.width();
+                } else {
+                    // Take up full height, leaving space for the popup.
+                    mPhotoEndParams.height = bounds.height() - 150;
+                    mPhotoEndParams.width = bounds.height() - 150;
+                }
+                mPhotoEndParams.topMargin = 0;
+                mPhotoEndParams.leftMargin = 0;
+                mPhotoEndParams.bottomMargin = mPhotoEndParams.height;
+                mPhotoEndParams.rightMargin = mPhotoEndParams.width;
+            }
+        }
+        return mPhotoEndParams;
+    }
+
+    private void animatePhotoOpen() {
+        mAnimationListener = new AnimatorListenerAdapter() {
+            private void capturePhotoPos() {
+                mPhotoView.requestLayout();
+                mOriginalPos.left = mPhotoView.getLeft();
+                mOriginalPos.top = mPhotoView.getTop();
+                mOriginalPos.right = mPhotoView.getRight();
+                mOriginalPos.bottom = mPhotoView.getBottom();
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                capturePhotoPos();
+                if (mPhotoHandler != null) {
+                    mPhotoHandler.onClick(mPhotoView);
+                }
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                capturePhotoPos();
+            }
+        };
+        animatePhoto(getPhotoEndParams());
+    }
+
+    private void closePhotoAndFinish() {
+        mAnimationListener = new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                // After the photo animates down, fade it away and finish.
+                ObjectAnimator anim = ObjectAnimator.ofFloat(
+                        mPhotoView, "alpha", 0f).setDuration(PHOTO_CONTRACT_DURATION);
+                anim.addListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        activityFinish();
+                    }
+                });
+                anim.start();
+            }
+        };
+
+        // TODO: This won't animate in the right way if the rotation has changed since the activity
+        // was first started.
+        animatePhoto(mPhotoStartParams);
+        animateAwayBackground();
+    }
+
+    private void animatePhoto(MarginLayoutParams to) {
+        // Cancel any existing animation.
+        if (mPhotoAnimator != null) {
+            mPhotoAnimator.cancel();
+        }
+
+        mPhotoView.setLayoutParams(to);
+        mAnimationPending = true;
+        mPhotoView.requestLayout();
+    }
+
+    private void animateInBackground() {
+        ObjectAnimator.ofFloat(mBackdrop, "alpha", 0, 0.5f).setDuration(
+                PHOTO_EXPAND_DURATION).start();
+    }
+
+    private void animateAwayBackground() {
+        ObjectAnimator.ofFloat(mBackdrop, "alpha", 0f).setDuration(
+                BACKDROP_FADEOUT_DURATION).start();
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        if (mCurrentPhotoFile != null) {
+            outState.putString(KEY_CURRENT_PHOTO_FILE, mCurrentPhotoFile.toString());
+        }
+        outState.putBoolean(KEY_SUB_ACTIVITY_IN_PROGRESS, mSubActivityInProgress);
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (mPhotoHandler != null) {
+            mSubActivityInProgress = false;
+            if (mPhotoHandler.handlePhotoActivityResult(requestCode, resultCode, data)) {
+                // Clear out any pending photo result.
+                mPendingPhotoResult = null;
+            } else {
+                // User returning to the photo selection activity.  Re-display options.
+                mPhotoHandler.onClick(mPhotoView);
+            }
+        } else {
+            // Create a pending photo result to be handled when the photo handler is created.
+            mPendingPhotoResult = new PendingPhotoResult(requestCode, resultCode, data);
+        }
+    }
+
+    private void attachPhotoHandler() {
+        mPhotoHandler = new PhotoHandler(this, mPhotoView,
+                PhotoActionPopup.MODE_NO_PHOTO, mState);
+        if (mPendingPhotoResult != null) {
+            mPhotoHandler.handlePhotoActivityResult(mPendingPhotoResult.mRequestCode,
+                    mPendingPhotoResult.mResultCode, mPendingPhotoResult.mData);
+            mPendingPhotoResult = null;
+        } else {
+            animatePhotoOpen();
+        }
+    }
+
+    private final class PhotoHandler extends PhotoSelectionHandler {
+        private PhotoHandler(Context context, View photoView, int photoMode,
+                EntityDeltaList state) {
+            super(context, photoView, photoMode, mIsDirectoryContact, state);
+            setListener(new PhotoListener(context, mIsProfile));
+        }
+
+        private final class PhotoListener extends PhotoActionListener {
+            private final Context mContext;
+            private final boolean mIsProfile;
+            private PhotoListener(Context context, boolean isProfile) {
+                mContext = context;
+                mIsProfile = isProfile;
+            }
+
+            @Override
+            public void startTakePhotoActivity(Intent intent, int requestCode, File photoFile) {
+                mSubActivityInProgress = true;
+                mCurrentPhotoFile = photoFile;
+                startActivityForResult(intent, requestCode);
+            }
+
+            @Override
+            public void startPickFromGalleryActivity(Intent intent, int requestCode) {
+                mSubActivityInProgress = true;
+                startActivityForResult(intent, requestCode);
+            }
+
+            @Override
+            public void onPhotoSelected(Bitmap bitmap) {
+                EntityDeltaList delta = getDeltaForAttachingPhotoToContact(bitmap);
+                Intent intent = ContactSaveService.createSaveContactIntent(mContext, delta,
+                        "", 0, mIsProfile, PhotoSelectionActivity.class,
+                        ContactEditorActivity.ACTION_SAVE_COMPLETED);
+                startService(intent);
+                finish();
+            }
+
+            @Override
+            public File getCurrentPhotoFile() {
+                return mCurrentPhotoFile;
+            }
+
+            @Override
+            public void onPhotoSelectionDismissed() {
+                if (!mSubActivityInProgress) {
+                    finish();
+                }
+            }
+        }
+    }
+
+    private static class PendingPhotoResult {
+        private int mRequestCode;
+        private int mResultCode;
+        private Intent mData;
+        private PendingPhotoResult(int requestCode, int resultCode, Intent data) {
+            mRequestCode = requestCode;
+            mResultCode = resultCode;
+            mData = data;
+        }
+    }
+}
diff --git a/src/com/android/contacts/detail/CarouselTab.java b/src/com/android/contacts/detail/CarouselTab.java
index 677f0ad..cdcf6b2 100644
--- a/src/com/android/contacts/detail/CarouselTab.java
+++ b/src/com/android/contacts/detail/CarouselTab.java
@@ -73,8 +73,9 @@
 
     @Override
     public void disableTouchInterceptor() {
-        // This shouldn't be called because there is no need to disable the touch interceptor if
-        // there is no content within the tab that needs to be clicked.
+        if (mTouchInterceptLayer != null) {
+            mTouchInterceptLayer.setVisibility(View.GONE);
+        }
     }
 
     @Override
diff --git a/src/com/android/contacts/detail/ContactDetailDisplayUtils.java b/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
index b81cebf..588e6ff 100644
--- a/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
+++ b/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
@@ -20,6 +20,8 @@
 import com.android.contacts.ContactLoader.Result;
 import com.android.contacts.ContactPhotoManager;
 import com.android.contacts.R;
+import com.android.contacts.activities.PhotoSelectionActivity;
+import com.android.contacts.model.EntityDeltaList;
 import com.android.contacts.preference.ContactsPreferences;
 import com.android.contacts.util.ContactBadgeUtil;
 import com.android.contacts.util.HtmlUtils;
@@ -27,19 +29,23 @@
 import com.android.contacts.util.StreamItemPhotoEntry;
 import com.google.common.annotations.VisibleForTesting;
 
+import android.app.Activity;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Entity;
 import android.content.Entity.NamedContentValues;
+import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Resources;
 import android.content.res.Resources.NotFoundException;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
+import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
+import android.os.Parcelable;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.CommonDataKinds.Organization;
 import android.provider.ContactsContract.Data;
@@ -51,6 +57,7 @@
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
+import android.view.View.OnClickListener;
 import android.view.ViewGroup;
 import android.view.animation.AccelerateInterpolator;
 import android.view.animation.AlphaAnimation;
@@ -192,11 +199,20 @@
     /**
      * Sets the contact photo to display in the given {@link ImageView}. If bitmap is null, the
      * default placeholder image is shown.
+     * @param context The context.
+     * @param contactData The contact loader result.
+     * @param photoView The photo view that will host the image and act as the basis for the
+     *     photo selector.
+     * @param expandPhotoOnClick Whether the photo should be expanded to fill more of the screen
+     *     when clicked.
+     * @return The onclick listener for the photo.  When clicked, a photo selection activity will
+     *     be launched.
      */
-    public static void setPhoto(Context context, Result contactData, ImageView photoView) {
+    public static OnClickListener setPhoto(Context context, Result contactData,
+            ImageView photoView, boolean expandPhotoOnClick) {
         if (contactData.isLoadingPhoto()) {
             photoView.setImageBitmap(null);
-            return;
+            return null;
         }
         byte[] photo = contactData.getPhotoBinaryData();
         Bitmap bitmap = photo != null ? BitmapFactory.decodeByteArray(photo, 0, photo.length)
@@ -209,6 +225,52 @@
             photoView.startAnimation(animation);
         }
         photoView.setImageBitmap(bitmap);
+
+        // Set up the photo to display a full-screen photo selection activity when clicked.
+        OnClickListener clickListener = new PhotoClickListener(context, contactData, bitmap,
+                expandPhotoOnClick);
+        photoView.setOnClickListener(clickListener);
+        return clickListener;
+    }
+
+    private static final class PhotoClickListener implements OnClickListener {
+
+        private final Context mContext;
+        private final Result mContactData;
+        private final Bitmap mPhotoBitmap;
+        private final boolean mExpandPhotoOnClick;
+        public PhotoClickListener(Context context, Result contactData, Bitmap photoBitmap,
+                boolean expandPhotoOnClick) {
+            mContext = context;
+            mContactData = contactData;
+            mPhotoBitmap = photoBitmap;
+            mExpandPhotoOnClick = expandPhotoOnClick;
+        }
+
+        @Override
+        public void onClick(View v) {
+            // Assemble the intent.
+            EntityDeltaList delta = EntityDeltaList.fromIterator(
+                    mContactData.getEntities().iterator());
+
+            // Find location and bounds of target view, adjusting based on the
+            // assumed local density.
+            final float appScale =
+                    mContext.getResources().getCompatibilityInfo().applicationScale;
+            final int[] pos = new int[2];
+            v.getLocationOnScreen(pos);
+
+            final Rect rect = new Rect();
+            rect.left = (int) (pos[0] * appScale + 0.5f);
+            rect.top = (int) (pos[1] * appScale + 0.5f);
+            rect.right = (int) ((pos[0] + v.getWidth()) * appScale + 0.5f);
+            rect.bottom = (int) ((pos[1] + v.getHeight()) * appScale + 0.5f);
+
+            Intent photoSelectionIntent = PhotoSelectionActivity.buildIntent(mContext,
+                    mPhotoBitmap, rect, delta, mContactData.isUserProfile(),
+                    mContactData.isDirectoryEntry(), mExpandPhotoOnClick);
+            mContext.startActivity(photoSelectionIntent);
+        }
     }
 
     /**
diff --git a/src/com/android/contacts/detail/ContactDetailFragment.java b/src/com/android/contacts/detail/ContactDetailFragment.java
index e74f481..577062e 100644
--- a/src/com/android/contacts/detail/ContactDetailFragment.java
+++ b/src/com/android/contacts/detail/ContactDetailFragment.java
@@ -40,8 +40,8 @@
 import com.android.contacts.util.Constants;
 import com.android.contacts.util.DataStatus;
 import com.android.contacts.util.DateUtils;
-import com.android.contacts.util.StructuredPostalUtils;
 import com.android.contacts.util.PhoneCapabilityTester;
+import com.android.contacts.util.StructuredPostalUtils;
 import com.android.contacts.widget.TransitionAnimationView;
 import com.android.internal.telephony.ITelephony;
 import com.google.common.annotations.VisibleForTesting;
@@ -147,7 +147,8 @@
     private Listener mListener;
 
     private ContactLoader.Result mContactData;
-    private ImageView mStaticPhotoView;
+    private ViewGroup mStaticPhotoContainer;
+    private View mPhotoTouchOverlay;
     private ListView mListView;
     private ViewAdapter mAdapter;
     private Uri mPrimaryPhoneUri = null;
@@ -162,7 +163,8 @@
 
     private final QuickFix[] mPotentialQuickFixes = new QuickFix[] {
             new MakeLocalCopyQuickFix(),
-            new AddToMyContactsQuickFix() };
+            new AddToMyContactsQuickFix()
+    };
 
     /**
      * Device capability: Set during buildEntries and used in the long-press context menu
@@ -280,7 +282,8 @@
 
         mInflater = inflater;
 
-        mStaticPhotoView = (ImageView) mView.findViewById(R.id.photo);
+        mStaticPhotoContainer = (ViewGroup) mView.findViewById(R.id.static_photo_container);
+        mPhotoTouchOverlay = mView.findViewById(R.id.photo_touch_intercept_overlay);
 
         mListView = (ListView) mView.findViewById(android.R.id.list);
         mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
@@ -442,16 +445,22 @@
         mContactHasSocialUpdates = !mContactData.getStreamItems().isEmpty();
 
         // Setup the photo if applicable
-        if (mStaticPhotoView != null) {
-            // The presence of a static photo view is not sufficient to determine whether or not
-            // we should show the photo. Check the mShowStaticPhoto flag which can be set by an
+        if (mStaticPhotoContainer != null) {
+            // The presence of a static photo container is not sufficient to determine whether or
+            // not we should show the photo. Check the mShowStaticPhoto flag which can be set by an
             // outside class depending on screen size, layout, and whether the contact has social
             // updates or not.
             if (mShowStaticPhoto) {
-                mStaticPhotoView.setVisibility(View.VISIBLE);
-                ContactDetailDisplayUtils.setPhoto(mContext, mContactData, mStaticPhotoView);
+                mStaticPhotoContainer.setVisibility(View.VISIBLE);
+                ImageView photoView = (ImageView) mStaticPhotoContainer.findViewById(R.id.photo);
+                OnClickListener listener = ContactDetailDisplayUtils.setPhoto(mContext,
+                        mContactData, photoView, !PhoneCapabilityTester.isUsingTwoPanes(mContext));
+                if (mPhotoTouchOverlay != null) {
+                    mPhotoTouchOverlay.setVisibility(View.VISIBLE);
+                    mPhotoTouchOverlay.setOnClickListener(listener);
+                }
             } else {
-                mStaticPhotoView.setVisibility(View.GONE);
+                mStaticPhotoContainer.setVisibility(View.GONE);
             }
         }
 
@@ -1371,10 +1380,11 @@
     /**
      * Cache of the children views for a view that displays a header view entry.
      */
-    private static class HeaderViewCache {
+    private static class HeaderViewCache implements ViewOverlay {
         public final TextView displayNameView;
         public final TextView companyView;
         public final ImageView photoView;
+        public final View photoOverlayView;
         public final CheckBox starredView;
         public final int layoutResourceId;
 
@@ -1382,9 +1392,30 @@
             displayNameView = (TextView) view.findViewById(R.id.name);
             companyView = (TextView) view.findViewById(R.id.company);
             photoView = (ImageView) view.findViewById(R.id.photo);
+            photoOverlayView = view.findViewById(R.id.photo_touch_intercept_overlay);
             starredView = (CheckBox) view.findViewById(R.id.star);
             layoutResourceId = layoutResourceInflated;
         }
+
+        @Override
+        public void setAlphaLayerValue(float alpha) {
+            // Nothing to do.
+        }
+
+        @Override
+        public void enableTouchInterceptor(OnClickListener clickListener) {
+            if (photoOverlayView != null) {
+                photoOverlayView.setVisibility(View.VISIBLE);
+                photoOverlayView.setOnClickListener(clickListener);
+            }
+        }
+
+        @Override
+        public void disableTouchInterceptor() {
+            if (photoOverlayView != null) {
+                photoOverlayView.setVisibility(View.GONE);
+            }
+        }
     }
 
     /**
@@ -1498,7 +1529,10 @@
 
             // Set the photo if it should be displayed
             if (viewCache.photoView != null) {
-                ContactDetailDisplayUtils.setPhoto(mContext, mContactData, viewCache.photoView);
+                OnClickListener listener = ContactDetailDisplayUtils.setPhoto(mContext,
+                        mContactData, viewCache.photoView,
+                        !PhoneCapabilityTester.isUsingTwoPanes(mContext));
+                viewCache.enableTouchInterceptor(listener);
             }
 
             // Set the starred state if it should be displayed
diff --git a/src/com/android/contacts/detail/ContactDetailTabCarousel.java b/src/com/android/contacts/detail/ContactDetailTabCarousel.java
index 045e900..e13d3c8 100644
--- a/src/com/android/contacts/detail/ContactDetailTabCarousel.java
+++ b/src/com/android/contacts/detail/ContactDetailTabCarousel.java
@@ -18,7 +18,9 @@
 
 import com.android.contacts.ContactLoader;
 import com.android.contacts.R;
+import com.android.contacts.util.PhoneCapabilityTester;
 
+import android.app.Activity;
 import android.content.Context;
 import android.content.res.Resources;
 import android.util.AttributeSet;
@@ -50,6 +52,8 @@
     private ImageView mPhotoView;
     private TextView mStatusView;
     private ImageView mStatusPhotoView;
+    private boolean mHasPhoto;
+    private OnClickListener mPhotoClickListener;
 
     private Listener mListener;
 
@@ -153,7 +157,11 @@
     private final OnClickListener mAboutTabTouchInterceptListener = new OnClickListener() {
         @Override
         public void onClick(View v) {
-            mListener.onTabSelected(TAB_INDEX_ABOUT);
+            if (mCurrentTab == TAB_INDEX_ABOUT && mPhotoClickListener != null) {
+                mPhotoClickListener.onClick(v);
+            } else {
+                mListener.onTabSelected(TAB_INDEX_ABOUT);
+            }
         }
     };
 
@@ -256,9 +264,11 @@
             case TAB_INDEX_ABOUT:
                 mAboutTab.showSelectedState();
                 mUpdatesTab.showDeselectedState();
+                mUpdatesTab.enableTouchInterceptor(mUpdatesTabTouchInterceptListener);
                 break;
             case TAB_INDEX_UPDATES:
                 mUpdatesTab.showSelectedState();
+                mUpdatesTab.disableTouchInterceptor();
                 mAboutTab.showDeselectedState();
                 break;
             default:
@@ -275,10 +285,12 @@
         if (contactData == null) {
             return;
         }
+        mHasPhoto = contactData.getPhotoUri() != null;
 
         // TODO: Move this into the {@link CarouselTab} class when the updates fragment code is more
         // finalized
-        ContactDetailDisplayUtils.setPhoto(mContext, contactData, mPhotoView);
+        mPhotoClickListener = ContactDetailDisplayUtils.setPhoto(mContext, contactData, mPhotoView,
+                !PhoneCapabilityTester.isUsingTwoPanes(mContext));
         ContactDetailDisplayUtils.setSocialSnippet(mContext, contactData, mStatusView,
                 mStatusPhotoView);
     }
diff --git a/src/com/android/contacts/detail/PhotoSelectionHandler.java b/src/com/android/contacts/detail/PhotoSelectionHandler.java
new file mode 100644
index 0000000..72397c0
--- /dev/null
+++ b/src/com/android/contacts/detail/PhotoSelectionHandler.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2011 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.detail;
+
+import com.android.contacts.R;
+import com.android.contacts.editor.PhotoActionPopup;
+import com.android.contacts.model.AccountType;
+import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.EntityDelta;
+import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.model.EntityDeltaList;
+import com.android.contacts.model.EntityModifier;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.media.MediaScannerConnection;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.DisplayPhoto;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ListPopupWindow;
+import android.widget.PopupWindow.OnDismissListener;
+import android.widget.Toast;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * Handles displaying a photo selection popup for a given photo view and dealing with the results
+ * that come back.
+ */
+public class PhotoSelectionHandler implements OnClickListener {
+
+    private static final String TAG = PhotoSelectionHandler.class.getSimpleName();
+
+    private static final File PHOTO_DIR = new File(
+            Environment.getExternalStorageDirectory() + "/DCIM/Camera");
+
+    private static final String PHOTO_DATE_FORMAT = "'IMG'_yyyyMMdd_HHmmss";
+
+    private static final int REQUEST_CODE_CAMERA_WITH_DATA = 1001;
+    private static final int REQUEST_CODE_PHOTO_PICKED_WITH_DATA = 1002;
+
+    private final Context mContext;
+    private final View mPhotoView;
+    private final int mPhotoMode;
+    private final int mPhotoPickSize;
+    private final EntityDeltaList mState;
+    private final boolean mIsDirectoryContact;
+    private ListPopupWindow mPopup;
+    private AccountType mWritableAccount;
+    private PhotoActionListener mListener;
+
+    public PhotoSelectionHandler(Context context, View photoView, int photoMode,
+            boolean isDirectoryContact, EntityDeltaList state) {
+        mContext = context;
+        mPhotoView = photoView;
+        mPhotoMode = photoMode;
+        mIsDirectoryContact = isDirectoryContact;
+        mState = state;
+        mPhotoPickSize = getPhotoPickSize();
+    }
+
+    public void destroy() {
+        if (mPopup != null) {
+            mPopup.dismiss();
+        }
+    }
+
+    public PhotoActionListener getListener() {
+        return mListener;
+    }
+
+    public void setListener(PhotoActionListener listener) {
+        mListener = listener;
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (mListener != null) {
+            if (getWritableEntityIndex() != -1) {
+                mPopup = PhotoActionPopup.createPopupMenu(
+                        mContext, mPhotoView, mListener, mPhotoMode);
+                mPopup.setOnDismissListener(new OnDismissListener() {
+                    @Override
+                    public void onDismiss() {
+                        mListener.onPhotoSelectionDismissed();
+                    }
+                });
+                mPopup.show();
+            }
+        }
+    }
+
+    /**
+     * Attempts to handle the given activity result.  Returns whether this handler was able to
+     * process the result successfully.
+     * @param requestCode The request code.
+     * @param resultCode The result code.
+     * @param data The intent that was returned.
+     * @return Whether the handler was able to process the result.
+     */
+    public boolean handlePhotoActivityResult(int requestCode, int resultCode, Intent data) {
+        if (resultCode == Activity.RESULT_OK) {
+            switch (requestCode) {
+                case REQUEST_CODE_PHOTO_PICKED_WITH_DATA: {
+                    Bitmap bitmap = data.getParcelableExtra("data");
+                    mListener.onPhotoSelected(bitmap);
+                    return true;
+                }
+                case REQUEST_CODE_CAMERA_WITH_DATA: {
+                    doCropPhoto(mListener.getCurrentPhotoFile());
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Return the index of the first entity in the contact data that belongs to a contact-writable
+     * account, or -1 if no such entity exists.
+     */
+    private int getWritableEntityIndex() {
+        // Directory entries are non-writable.
+        if (mIsDirectoryContact) {
+            return -1;
+        }
+
+        // Find the first writable entity.
+        int entityIndex = 0;
+        for (EntityDelta delta : mState) {
+            ContentValues entityValues = delta.getValues().getCompleteValues();
+            String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
+            String dataSet = entityValues.getAsString(RawContacts.DATA_SET);
+            AccountType accountType = AccountTypeManager.getInstance(mContext).getAccountType(
+                    type, dataSet);
+            if (accountType.areContactsWritable()) {
+                mWritableAccount = accountType;
+                return entityIndex;
+            }
+            entityIndex++;
+        }
+        return -1;
+    }
+
+    /**
+     * Utility method to retrieve the entity delta for attaching the given bitmap to the contact.
+     * This will attach the photo to the first contact-writable account that provided data to the
+     * contact.  It is the caller's responsibility to apply the delta.
+     * @param bitmap The photo to use.
+     * @return An entity delta list that can be applied to associate the bitmap with the contact,
+     *     or null if the photo could not be parsed or none of the accounts associated with the
+     *     contact are writable.
+     */
+    public EntityDeltaList getDeltaForAttachingPhotoToContact(Bitmap bitmap) {
+        // Find the first writable entity.
+        int writableEntityIndex = getWritableEntityIndex();
+        if (writableEntityIndex != -1) {
+            // Convert the photo to a byte array.
+            final int size = bitmap.getWidth() * bitmap.getHeight() * 4;
+            final ByteArrayOutputStream out = new ByteArrayOutputStream(size);
+
+            try {
+                bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
+                out.flush();
+                out.close();
+            } catch (IOException e) {
+                Log.w(TAG, "Unable to serialize photo: " + e.toString());
+                return null;
+            }
+
+            // Note - guaranteed to have contact data if we have a writable entity index.
+            EntityDelta delta = mState.get(writableEntityIndex);
+            ValuesDelta child = EntityModifier.ensureKindExists(
+                    delta, mWritableAccount, Photo.CONTENT_ITEM_TYPE);
+            child.put(Photo.PHOTO, out.toByteArray());
+            child.setFromTemplate(false);
+            child.put(Photo.IS_SUPER_PRIMARY, 1);
+
+            return mState;
+        }
+        return null;
+    }
+
+    /**
+     * Sends a newly acquired photo to Gallery for cropping
+     */
+    private void doCropPhoto(File f) {
+        try {
+            // Add the image to the media store
+            MediaScannerConnection.scanFile(
+                    mContext,
+                    new String[] { f.getAbsolutePath() },
+                    new String[] { null },
+                    null);
+
+            // Launch gallery to crop the photo
+            final Intent intent = getCropImageIntent(Uri.fromFile(f));
+            mListener.startPickFromGalleryActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA);
+        } catch (Exception e) {
+            Log.e(TAG, "Cannot crop image", e);
+            Toast.makeText(mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
+        }
+    }
+
+    private String getPhotoFileName() {
+        Date date = new Date(System.currentTimeMillis());
+        SimpleDateFormat dateFormat = new SimpleDateFormat(PHOTO_DATE_FORMAT);
+        return dateFormat.format(date) + ".jpg";
+    }
+
+    private int getPhotoPickSize() {
+        // Note that this URI is safe to call on the UI thread.
+        Cursor c = mContext.getContentResolver().query(DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI,
+                new String[]{DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null);
+        try {
+            c.moveToFirst();
+            return c.getInt(0);
+        } finally {
+            c.close();
+        }
+    }
+
+    /**
+     * Constructs an intent for picking a photo from Gallery, cropping it and returning the bitmap.
+     */
+    private Intent getPhotoPickIntent() {
+        Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
+        intent.setType("image/*");
+        intent.putExtra("crop", "true");
+        intent.putExtra("aspectX", 1);
+        intent.putExtra("aspectY", 1);
+        intent.putExtra("outputX", mPhotoPickSize);
+        intent.putExtra("outputY", mPhotoPickSize);
+        intent.putExtra("return-data", true);
+        return intent;
+    }
+
+    /**
+     * Constructs an intent for image cropping.
+     */
+    private Intent getCropImageIntent(Uri photoUri) {
+        Intent intent = new Intent("com.android.camera.action.CROP");
+        intent.setDataAndType(photoUri, "image/*");
+        intent.putExtra("crop", "true");
+        intent.putExtra("aspectX", 1);
+        intent.putExtra("aspectY", 1);
+        intent.putExtra("outputX", mPhotoPickSize);
+        intent.putExtra("outputY", mPhotoPickSize);
+        intent.putExtra("return-data", true);
+        return intent;
+    }
+
+    /**
+     * Constructs an intent for capturing a photo and storing it in a temporary file.
+     */
+    public static Intent getTakePhotoIntent(File f) {
+        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE, null);
+        intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(f));
+        return intent;
+    }
+
+    public abstract class PhotoActionListener implements PhotoActionPopup.Listener {
+        @Override
+        public void onUseAsPrimaryChosen() {
+            // No default implementation.
+        }
+
+        @Override
+        public void onRemovePictureChosen() {
+            // No default implementation.
+        }
+
+        @Override
+        public void onTakePhotoChosen() {
+            try {
+                // Launch camera to take photo for selected contact
+                PHOTO_DIR.mkdirs();
+                File photoFile = new File(PHOTO_DIR, getPhotoFileName());
+                startTakePhotoActivity(getTakePhotoIntent(photoFile),
+                        REQUEST_CODE_CAMERA_WITH_DATA, photoFile);
+            } catch (ActivityNotFoundException e) {
+                Toast.makeText(mContext, R.string.photoPickerNotFoundText,
+                        Toast.LENGTH_LONG).show();
+            }
+        }
+
+        @Override
+        public void onPickFromGalleryChosen() {
+            try {
+                // Launch picker to choose photo for selected contact
+                final Intent intent = getPhotoPickIntent();
+                startPickFromGalleryActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA);
+            } catch (ActivityNotFoundException e) {
+                Toast.makeText(mContext, R.string.photoPickerNotFoundText,
+                        Toast.LENGTH_LONG).show();
+            }
+        }
+
+        /**
+         * Should initiate an activity to take a photo using the camera.
+         * @param intent The image capture intent.
+         * @param requestCode The request code to use, suitable for handling by
+         *     {@link PhotoSelectionHandler#handlePhotoActivityResult(int, int, Intent)}.
+         * @param photoFile The file path that will be used to store the photo.  This is generally
+         *     what should be returned by
+         *     {@link PhotoSelectionHandler.PhotoActionListener#getCurrentPhotoFile()}.
+         */
+        public abstract void startTakePhotoActivity(Intent intent, int requestCode, File photoFile);
+
+        /**
+         * Should initiate an activity pick a photo from the gallery.
+         * @param intent The image capture intent.
+         * @param requestCode The request code to use, suitable for handling by
+         *     {@link PhotoSelectionHandler#handlePhotoActivityResult(int, int, Intent)}.
+         */
+        public abstract void startPickFromGalleryActivity(Intent intent, int requestCode);
+
+        /**
+         * Called when the user has completed selection of a photo.
+         * @param bitmap The selected and cropped photo.
+         */
+        public abstract void onPhotoSelected(Bitmap bitmap);
+
+        /**
+         * Gets the current photo file that is being interacted with.  It is the activity or
+         * fragment's responsibility to maintain this in saved state, since this handler instance
+         * will not survive rotation.
+         */
+        public abstract File getCurrentPhotoFile();
+
+        /**
+         * Called when the photo selection dialog is dismissed.
+         */
+        public abstract void onPhotoSelectionDismissed();
+    }
+}
diff --git a/src/com/android/contacts/detail/TransformableImageView.java b/src/com/android/contacts/detail/TransformableImageView.java
new file mode 100644
index 0000000..6edc42b
--- /dev/null
+++ b/src/com/android/contacts/detail/TransformableImageView.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2011 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.detail;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageView;
+
+/**
+ * Extension to ImageView that handles cropping during resize animations.
+ */
+public class TransformableImageView extends ImageView {
+
+    public TransformableImageView(Context context) {
+        super(context);
+    }
+
+    public TransformableImageView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public TransformableImageView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        int saveCount = canvas.getSaveCount();
+        canvas.save();
+        canvas.translate(mPaddingLeft, mPaddingTop);
+        Matrix drawMatrix = new Matrix();
+        int dwidth = getDrawable().getIntrinsicWidth();
+        int dheight = getDrawable().getIntrinsicHeight();
+
+        int vwidth = getWidth() - mPaddingLeft - mPaddingRight;
+        int vheight = getHeight() - mPaddingTop - mPaddingBottom;
+        float scale;
+        float dx = 0, dy = 0;
+
+        if (dwidth * vheight > vwidth * dheight) {
+            scale = (float) vheight / (float) dheight;
+            dx = (vwidth - dwidth * scale) * 0.5f;
+        } else {
+            scale = (float) vwidth / (float) dwidth;
+            dy = (vheight - dheight * scale) * 0.5f;
+        }
+
+        drawMatrix.setScale(scale, scale);
+        drawMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));
+        canvas.concat(drawMatrix);
+        getDrawable().draw(canvas);
+        canvas.restoreToCount(saveCount);
+    }
+}
diff --git a/src/com/android/contacts/editor/ContactEditorFragment.java b/src/com/android/contacts/editor/ContactEditorFragment.java
index da5237f..5448005 100644
--- a/src/com/android/contacts/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorFragment.java
@@ -23,6 +23,7 @@
 import com.android.contacts.activities.ContactEditorAccountsChangedActivity;
 import com.android.contacts.activities.ContactEditorActivity;
 import com.android.contacts.activities.JoinContactActivity;
+import com.android.contacts.detail.PhotoSelectionHandler;
 import com.android.contacts.editor.AggregationSuggestionEngine.Suggestion;
 import com.android.contacts.editor.Editor.EditorListener;
 import com.android.contacts.model.AccountType;
@@ -44,7 +45,6 @@
 import android.app.Fragment;
 import android.app.LoaderManager;
 import android.app.LoaderManager.LoaderCallbacks;
-import android.content.ActivityNotFoundException;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
@@ -56,10 +56,8 @@
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.Rect;
-import android.media.MediaScannerConnection;
 import android.net.Uri;
 import android.os.Bundle;
-import android.os.Environment;
 import android.os.SystemClock;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.Event;
@@ -67,11 +65,9 @@
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.DisplayPhoto;
 import android.provider.ContactsContract.Groups;
 import android.provider.ContactsContract.Intents;
 import android.provider.ContactsContract.RawContacts;
-import android.provider.MediaStore;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.Menu;
@@ -87,11 +83,9 @@
 import android.widget.Toast;
 
 import java.io.File;
-import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
-import java.util.Date;
 import java.util.List;
 
 public class ContactEditorFragment extends Fragment implements
@@ -192,26 +186,19 @@
     }
 
     private static final int REQUEST_CODE_JOIN = 0;
-    private static final int REQUEST_CODE_CAMERA_WITH_DATA = 1;
-    private static final int REQUEST_CODE_PHOTO_PICKED_WITH_DATA = 2;
-    private static final int REQUEST_CODE_ACCOUNTS_CHANGED = 3;
+    private static final int REQUEST_CODE_ACCOUNTS_CHANGED = 1;
 
     private Bitmap mPhoto = null;
     private long mRawContactIdRequestingPhoto = -1;
     private long mRawContactIdRequestingPhotoAfterLoad = -1;
+    private PhotoSelectionHandler mPhotoSelectionHandler;
 
     private final EntityDeltaComparator mComparator = new EntityDeltaComparator();
 
-    private static final File PHOTO_DIR = new File(
-            Environment.getExternalStorageDirectory() + "/DCIM/Camera");
-
     private Cursor mGroupMetaData;
 
     private File mCurrentPhotoFile;
 
-    // Height/width (in pixels) to request for the photo - queried from the provider.
-    private int mPhotoPickSize;
-
     private Context mContext;
     private String mAction;
     private Uri mLookupUri;
@@ -322,7 +309,6 @@
         super.onAttach(activity);
         mContext = activity;
         mEditorUtils = ContactEditorUtils.getInstance(mContext);
-        loadPhotoPickSize();
     }
 
     @Override
@@ -729,8 +715,9 @@
 
             editor.setState(entity, type, mViewIdGenerator, isEditingUserProfile());
 
-            editor.getPhotoEditor().setEditorListener(
-                    new PhotoEditorListener(editor, type.areContactsWritable()));
+            // Set up the photo handler.
+            bindPhotoHandler(editor, type, mState);
+
             if (editor instanceof RawContactEditorView) {
                 final RawContactEditorView rawContactEditor = (RawContactEditorView) editor;
                 EditorListener listener = new EditorListener() {
@@ -776,7 +763,32 @@
         // Activity can be null if we have been detached from the Activity
         final Activity activity = getActivity();
         if (activity != null) activity.invalidateOptionsMenu();
+    }
 
+    private void bindPhotoHandler(BaseRawContactEditorView editor, AccountType type,
+            EntityDeltaList state) {
+        final int mode;
+        if (type.areContactsWritable()) {
+            if (editor.hasSetPhoto()) {
+                if (hasMoreThanOnePhoto()) {
+                    mode = PhotoActionPopup.MODE_PHOTO_ALLOW_PRIMARY;
+                } else {
+                    mode = PhotoActionPopup.MODE_PHOTO_DISALLOW_PRIMARY;
+                }
+            } else {
+                mode = PhotoActionPopup.MODE_NO_PHOTO;
+            }
+        } else {
+            if (editor.hasSetPhoto() && hasMoreThanOnePhoto()) {
+                mode = PhotoActionPopup.MODE_READ_ONLY_ALLOW_PRIMARY;
+            } else {
+                // Read-only and either no photo or the only photo ==> no options
+                return;
+            }
+        }
+        mPhotoSelectionHandler = new PhotoHandler(mContext, editor, mode, state);
+        editor.getPhotoEditor().setEditorListener(
+                (PhotoHandler.PhotoEditorListener) mPhotoSelectionHandler.getListener());
     }
 
     private void bindGroupMetaData() {
@@ -926,32 +938,6 @@
         return save(SaveMode.JOIN);
     }
 
-    private void loadPhotoPickSize() {
-        Cursor c = mContext.getContentResolver().query(DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI,
-                new String[]{DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null);
-        try {
-            c.moveToFirst();
-            mPhotoPickSize = c.getInt(0);
-        } finally {
-            c.close();
-        }
-    }
-
-    /**
-     * Constructs an intent for picking a photo from Gallery, cropping it and returning the bitmap.
-     */
-    public Intent getPhotoPickIntent() {
-        Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
-        intent.setType("image/*");
-        intent.putExtra("crop", "true");
-        intent.putExtra("aspectX", 1);
-        intent.putExtra("aspectY", 1);
-        intent.putExtra("outputX", mPhotoPickSize);
-        intent.putExtra("outputY", mPhotoPickSize);
-        intent.putExtra("return-data", true);
-        return intent;
-    }
-
     /**
      * Check if our internal {@link #mState} is valid, usually checked before
      * performing user actions.
@@ -961,61 +947,6 @@
     }
 
     /**
-     * Create a file name for the icon photo using current time.
-     */
-    private String getPhotoFileName() {
-        Date date = new Date(System.currentTimeMillis());
-        SimpleDateFormat dateFormat = new SimpleDateFormat("'IMG'_yyyyMMdd_HHmmss");
-        return dateFormat.format(date) + ".jpg";
-    }
-
-    /**
-     * Constructs an intent for capturing a photo and storing it in a temporary file.
-     */
-    public static Intent getTakePickIntent(File f) {
-        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE, null);
-        intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(f));
-        return intent;
-    }
-
-    /**
-     * Sends a newly acquired photo to Gallery for cropping
-     */
-    protected void doCropPhoto(File f) {
-        try {
-            // Add the image to the media store
-            MediaScannerConnection.scanFile(
-                    mContext,
-                    new String[] { f.getAbsolutePath() },
-                    new String[] { null },
-                    null);
-
-            // Launch gallery to crop the photo
-            final Intent intent = getCropImageIntent(Uri.fromFile(f));
-            mStatus = Status.SUB_ACTIVITY;
-            startActivityForResult(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA);
-        } catch (Exception e) {
-            Log.e(TAG, "Cannot crop image", e);
-            Toast.makeText(mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
-        }
-    }
-
-    /**
-     * Constructs an intent for image cropping.
-     */
-    public Intent getCropImageIntent(Uri photoUri) {
-        Intent intent = new Intent("com.android.camera.action.CROP");
-        intent.setDataAndType(photoUri, "image/*");
-        intent.putExtra("crop", "true");
-        intent.putExtra("aspectX", 1);
-        intent.putExtra("aspectY", 1);
-        intent.putExtra("outputX", mPhotoPickSize);
-        intent.putExtra("outputY", mPhotoPickSize);
-        intent.putExtra("return-data", true);
-        return intent;
-    }
-
-    /**
      * Saves or creates the contact based on the mode, and if successful
      * finishes the activity.
      */
@@ -1591,27 +1522,13 @@
             mStatus = Status.EDITING;
         }
 
-        switch (requestCode) {
-            case REQUEST_CODE_PHOTO_PICKED_WITH_DATA: {
-                // Ignore failed requests
-                if (resultCode != Activity.RESULT_OK) return;
-                // As we are coming back to this view, the editor will be reloaded automatically,
-                // which will cause the photo that is set here to disappear. To prevent this,
-                // we remember to set a flag which is interpreted after loading.
-                // This photo is set here already to reduce flickering.
-                mPhoto = data.getParcelableExtra("data");
-                setPhoto(mRawContactIdRequestingPhoto, mPhoto);
-                mRawContactIdRequestingPhotoAfterLoad = mRawContactIdRequestingPhoto;
-                mRawContactIdRequestingPhoto = -1;
+        // See if the photo selection handler handles this result.
+        if (mPhotoSelectionHandler != null && mPhotoSelectionHandler.handlePhotoActivityResult(
+                requestCode, resultCode, data)) {
+            return;
+        }
 
-                break;
-            }
-            case REQUEST_CODE_CAMERA_WITH_DATA: {
-                // Ignore failed requests
-                if (resultCode != Activity.RESULT_OK) return;
-                doCropPhoto(mCurrentPhotoFile);
-                break;
-            }
+        switch (requestCode) {
             case REQUEST_CODE_JOIN: {
                 // Ignore failed requests
                 if (resultCode != Activity.RESULT_OK) return;
@@ -1770,111 +1687,97 @@
         save(SaveMode.SPLIT);
     }
 
-    private final class PhotoEditorListener
-            implements EditorListener, PhotoActionPopup.Listener {
-        private final BaseRawContactEditorView mEditor;
-        private final boolean mAccountWritable;
-
-        private PhotoEditorListener(BaseRawContactEditorView editor, boolean accountWritable) {
-            mEditor = editor;
-            mAccountWritable = accountWritable;
+    /**
+     * Custom photo handler for the editor.  The inner listener that this creates also has a
+     * reference to the editor and acts as an {@link EditorListener}, and uses that editor to hold
+     * state information in several of the listener methods.
+     */
+    private final class PhotoHandler extends PhotoSelectionHandler {
+        public PhotoHandler(Context context, BaseRawContactEditorView editor, int photoMode,
+                EntityDeltaList state) {
+            super(context, editor.getPhotoEditor(), photoMode, false, state);
+            setListener(new PhotoEditorListener(editor));
         }
 
-        @Override
-        public void onRequest(int request) {
-            if (!hasValidState()) return;
+        private final class PhotoEditorListener extends PhotoSelectionHandler.PhotoActionListener
+                implements EditorListener {
+            private final BaseRawContactEditorView mEditor;
 
-            if (request == EditorListener.REQUEST_PICK_PHOTO) {
-                // Determine mode
-                final int mode;
-                if (mAccountWritable) {
-                    if (mEditor.hasSetPhoto()) {
-                        if (hasMoreThanOnePhoto()) {
-                            mode = PhotoActionPopup.MODE_PHOTO_ALLOW_PRIMARY;
-                        } else {
-                            mode = PhotoActionPopup.MODE_PHOTO_DISALLOW_PRIMARY;
-                        }
-                    } else {
-                        mode = PhotoActionPopup.MODE_NO_PHOTO;
-                    }
-                } else {
-                    if (mEditor.hasSetPhoto() && hasMoreThanOnePhoto()) {
-                        mode = PhotoActionPopup.MODE_READ_ONLY_ALLOW_PRIMARY;
-                    } else {
-                        // Read-only and either no photo or the only photo ==> no options
-                        return;
-                    }
-                }
-                PhotoActionPopup.createPopupMenu(mContext, mEditor.getPhotoEditor(), this, mode)
-                        .show();
+            private PhotoEditorListener(BaseRawContactEditorView editor) {
+                mEditor = editor;
             }
-        }
 
-        @Override
-        public void onDeleteRequested(Editor removedEditor) {
-            // The picture cannot be deleted, it can only be removed, which is handled by
-            // onRemovePictureChosen()
-        }
+            @Override
+            public void onRequest(int request) {
+                if (!hasValidState()) return;
 
-        /**
-         * User has chosen to set the selected photo as the (super) primary photo
-         */
-        @Override
-        public void onUseAsPrimaryChosen() {
-            // Set the IsSuperPrimary for each editor
-            int count = mContent.getChildCount();
-            for (int i = 0; i < count; i++) {
-                final View childView = mContent.getChildAt(i);
-                if (childView instanceof BaseRawContactEditorView) {
-                    final BaseRawContactEditorView editor = (BaseRawContactEditorView) childView;
-                    final PhotoEditorView photoEditor = editor.getPhotoEditor();
-                    photoEditor.setSuperPrimary(editor == mEditor);
+                if (request == EditorListener.REQUEST_PICK_PHOTO) {
+                    onClick(mEditor.getPhotoEditor());
                 }
             }
-        }
 
-        /**
-         * User has chosen to remove a picture
-         */
-        @Override
-        public void onRemovePictureChosen() {
-            mEditor.setPhotoBitmap(null);
-        }
-
-        /**
-         * Launches Camera to take a picture and store it in a file.
-         */
-        @Override
-        public void onTakePhotoChosen() {
-            mRawContactIdRequestingPhoto = mEditor.getRawContactId();
-            try {
-                // Launch camera to take photo for selected contact
-                PHOTO_DIR.mkdirs();
-                mCurrentPhotoFile = new File(PHOTO_DIR, getPhotoFileName());
-                final Intent intent = getTakePickIntent(mCurrentPhotoFile);
-
-                mStatus = Status.SUB_ACTIVITY;
-                startActivityForResult(intent, REQUEST_CODE_CAMERA_WITH_DATA);
-            } catch (ActivityNotFoundException e) {
-                Toast.makeText(mContext, R.string.photoPickerNotFoundText,
-                        Toast.LENGTH_LONG).show();
+            @Override
+            public void onDeleteRequested(Editor removedEditor) {
+                // The picture cannot be deleted, it can only be removed, which is handled by
+                // onRemovePictureChosen()
             }
-        }
 
-        /**
-         * Launches Gallery to pick a photo.
-         */
-        @Override
-        public void onPickFromGalleryChosen() {
-            mRawContactIdRequestingPhoto = mEditor.getRawContactId();
-            try {
-                // Launch picker to choose photo for selected contact
-                final Intent intent = getPhotoPickIntent();
+            /**
+             * User has chosen to set the selected photo as the (super) primary photo
+             */
+            @Override
+            public void onUseAsPrimaryChosen() {
+                // Set the IsSuperPrimary for each editor
+                int count = mContent.getChildCount();
+                for (int i = 0; i < count; i++) {
+                    final View childView = mContent.getChildAt(i);
+                    if (childView instanceof BaseRawContactEditorView) {
+                        final BaseRawContactEditorView editor =
+                                (BaseRawContactEditorView) childView;
+                        final PhotoEditorView photoEditor = editor.getPhotoEditor();
+                        photoEditor.setSuperPrimary(editor == mEditor);
+                    }
+                }
+            }
+
+            /**
+             * User has chosen to remove a picture
+             */
+            @Override
+            public void onRemovePictureChosen() {
+                mEditor.setPhotoBitmap(null);
+            }
+
+            @Override
+            public void startTakePhotoActivity(Intent intent, int requestCode, File photoFile) {
+                mRawContactIdRequestingPhoto = mEditor.getRawContactId();
                 mStatus = Status.SUB_ACTIVITY;
-                startActivityForResult(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA);
-            } catch (ActivityNotFoundException e) {
-                Toast.makeText(mContext, R.string.photoPickerNotFoundText,
-                        Toast.LENGTH_LONG).show();
+                mCurrentPhotoFile = photoFile;
+                startActivityForResult(intent, requestCode);
+            }
+
+            @Override
+            public void startPickFromGalleryActivity(Intent intent, int requestCode) {
+                mRawContactIdRequestingPhoto = mEditor.getRawContactId();
+                mStatus = Status.SUB_ACTIVITY;
+                startActivityForResult(intent, requestCode);
+            }
+
+            @Override
+            public void onPhotoSelected(Bitmap bitmap) {
+                setPhoto(mRawContactIdRequestingPhoto, bitmap);
+                mRawContactIdRequestingPhotoAfterLoad = mRawContactIdRequestingPhoto;
+                mRawContactIdRequestingPhoto = -1;
+            }
+
+            @Override
+            public File getCurrentPhotoFile() {
+                return mCurrentPhotoFile;
+            }
+
+            @Override
+            public void onPhotoSelectionDismissed() {
+                // Nothing to do.
             }
         }
     }
diff --git a/src/com/android/contacts/editor/PhotoActionPopup.java b/src/com/android/contacts/editor/PhotoActionPopup.java
index cca6f9d..029212f 100644
--- a/src/com/android/contacts/editor/PhotoActionPopup.java
+++ b/src/com/android/contacts/editor/PhotoActionPopup.java
@@ -76,8 +76,6 @@
             @Override
             public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                 final ChoiceListItem choice = choices.get(position);
-                listPopupWindow.dismiss();
-
                 switch (choice.getId()) {
                     case ChoiceListItem.ID_USE_AS_PRIMARY:
                         listener.onUseAsPrimaryChosen();
@@ -92,6 +90,8 @@
                         listener.onPickFromGalleryChosen();
                         break;
                 }
+
+                listPopupWindow.dismiss();
             }
         };
 
diff --git a/src/com/android/contacts/model/EntityModifier.java b/src/com/android/contacts/model/EntityModifier.java
index 289ca54..ef5d304 100644
--- a/src/com/android/contacts/model/EntityModifier.java
+++ b/src/com/android/contacts/model/EntityModifier.java
@@ -103,19 +103,28 @@
     /**
      * Ensure that at least one of the given {@link DataKind} exists in the
      * given {@link EntityDelta} state, and try creating one if none exist.
+     * @return The child (either newly created or the first existing one), or null if the
+     *     account doesn't support this {@link DataKind}.
      */
-    public static void ensureKindExists(
+    public static ValuesDelta ensureKindExists(
             EntityDelta state, AccountType accountType, String mimeType) {
         final DataKind kind = accountType.getKindForMimetype(mimeType);
         final boolean hasChild = state.getMimeEntriesCount(mimeType, true) > 0;
 
-        if (!hasChild && kind != null) {
-            // Create child when none exists and valid kind
-            final ValuesDelta child = insertChild(state, kind);
-            if (kind.mimeType.equals(Photo.CONTENT_ITEM_TYPE)) {
-                child.setFromTemplate(true);
+        if (kind != null) {
+            if (hasChild) {
+                // Return the first entry.
+                return state.getMimeEntries(mimeType).get(0);
+            } else {
+                // Create child when none exists and valid kind
+                final ValuesDelta child = insertChild(state, kind);
+                if (kind.mimeType.equals(Photo.CONTENT_ITEM_TYPE)) {
+                    child.setFromTemplate(true);
+                }
+                return child;
             }
         }
+        return null;
     }
 
     /**