Merge "Delete create icons." into ub-contactsdialer-a-dev
diff --git a/res/drawable-hdpi/ic_check_circle_googblue_drawable_24dp.png b/res/drawable-hdpi/ic_check_circle_googblue_drawable_24dp.png
new file mode 100644
index 0000000..24d308f
--- /dev/null
+++ b/res/drawable-hdpi/ic_check_circle_googblue_drawable_24dp.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_check_circle_googblue_drawable24dp.png b/res/drawable-mdpi/ic_check_circle_googblue_drawable24dp.png
new file mode 100644
index 0000000..cea7967
--- /dev/null
+++ b/res/drawable-mdpi/ic_check_circle_googblue_drawable24dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_check_circle_googblue_drawable_24dp.png b/res/drawable-xhdpi/ic_check_circle_googblue_drawable_24dp.png
new file mode 100644
index 0000000..5cfcaf5
--- /dev/null
+++ b/res/drawable-xhdpi/ic_check_circle_googblue_drawable_24dp.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_check_circle_googblue_drawable_24dp.png b/res/drawable-xxhdpi/ic_check_circle_googblue_drawable_24dp.png
new file mode 100644
index 0000000..e50091c
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_check_circle_googblue_drawable_24dp.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/ic_check_circle_googblue_drawable_24dp.png b/res/drawable-xxxhdpi/ic_check_circle_googblue_drawable_24dp.png
new file mode 100644
index 0000000..c26bd2f
--- /dev/null
+++ b/res/drawable-xxxhdpi/ic_check_circle_googblue_drawable_24dp.png
Binary files differ
diff --git a/res/layout/compact_contact_editor_activity.xml b/res/layout/compact_contact_editor_activity.xml
index db847b0..595f381 100644
--- a/res/layout/compact_contact_editor_activity.xml
+++ b/res/layout/compact_contact_editor_activity.xml
@@ -15,8 +15,8 @@
     limitations under the License.
 -->
 
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/compact_contact_editor_fragment_container"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"/>
-
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/fragment_container"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"/>
diff --git a/res/layout/compact_photo_selection_fragment.xml b/res/layout/compact_photo_selection_fragment.xml
new file mode 100644
index 0000000..354b9f3
--- /dev/null
+++ b/res/layout/compact_photo_selection_fragment.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2015 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.
+-->
+<GridView xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/grid_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:numColumns="auto_fit"
+        android:columnWidth="100dp"
+        android:stretchMode="none"
+        android:gravity="center"
+        android:drawSelectorOnTop="true"/>
+
diff --git a/res/layout/compact_photo_selection_item.xml b/res/layout/compact_photo_selection_item.xml
new file mode 100644
index 0000000..6f70c80
--- /dev/null
+++ b/res/layout/compact_photo_selection_item.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2015 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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        style="@style/SelectableItem"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+    <ImageView
+            android:id="@+id/image"
+            android:layout_width="125dp"
+            android:layout_height="125dp"
+            android:adjustViewBounds="true"
+            android:layout_centerInParent="true"
+            android:scaleType="centerCrop" />
+
+    <ImageView
+            android:id="@+id/check"
+            android:layout_width="30dp"
+            android:layout_height="30dp"
+            android:layout_alignParentTop="true"
+            android:layout_alignParentStart="true"
+            android:src="@drawable/ic_check_circle_googblue_drawable_24dp"
+            android:visibility="gone"/>
+
+    <ImageView
+            android:id="@+id/account_type"
+            android:layout_width="30dp"
+            android:layout_height="30dp"
+            android:layout_alignParentBottom="true"
+            android:layout_alignParentStart="true"/>
+
+</RelativeLayout>
diff --git a/res/menu/edit_contact_photo.xml b/res/menu/edit_contact_photo.xml
new file mode 100644
index 0000000..725ea8b
--- /dev/null
+++ b/res/menu/edit_contact_photo.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2015 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.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+            android:id="@+id/menu_photo"
+            android:showAsAction="always"
+            android:icon="@drawable/ic_photo_camera_white_24dp"
+            android:title="@string/menu_change_photo"/>
+</menu>
diff --git a/src/com/android/contacts/activities/CompactContactEditorActivity.java b/src/com/android/contacts/activities/CompactContactEditorActivity.java
index 28dd8a5..358740e 100644
--- a/src/com/android/contacts/activities/CompactContactEditorActivity.java
+++ b/src/com/android/contacts/activities/CompactContactEditorActivity.java
@@ -17,19 +17,110 @@
 package com.android.contacts.activities;
 
 import com.android.contacts.R;
-import com.android.contacts.editor.CompactContactEditorFragment;
 import com.android.contacts.common.activity.RequestPermissionsActivity;
+import com.android.contacts.common.model.RawContactDeltaList;
+import com.android.contacts.detail.PhotoSelectionHandler;
+import com.android.contacts.editor.CompactContactEditorFragment;
+import com.android.contacts.editor.CompactPhotoSelectionFragment;
+import com.android.contacts.editor.PhotoSourceDialogFragment;
 
+import android.app.FragmentTransaction;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
 
+import java.io.FileNotFoundException;
+import java.util.ArrayList;
+
 /**
  * Contact editor with only the most important fields displayed initially.
  */
-public class CompactContactEditorActivity extends ContactEditorBaseActivity {
+public class CompactContactEditorActivity extends ContactEditorBaseActivity implements
+        PhotoSourceDialogFragment.Listener, CompactPhotoSelectionFragment.Listener {
 
     private static final String TAG_COMPACT_EDITOR = "compact_editor";
+    private static final String TAG_PHOTO_SELECTION = "photo_selector";
+
+    private static final String STATE_PHOTO_MODE = "photo_mode";
+    private static final String STATE_IS_PHOTO_SELECTION = "is_photo_selection";
+
+    /**
+     * Displays a PopupWindow with photo edit options.
+     */
+    private final class CompactPhotoSelectionHandler extends PhotoSelectionHandler {
+
+        /**
+         * Receiver of photo edit option callbacks.
+         */
+        private final class CompactPhotoActionListener extends PhotoActionListener {
+
+            @Override
+            public void onRemovePictureChosen() {
+                getEditorFragment().removePhoto();
+                if (mIsPhotoSelection) {
+                    showEditorFragment();
+                }
+            }
+
+            @Override
+            public void onPhotoSelected(Uri uri) throws FileNotFoundException {
+                mPhotoUri = uri;
+                getEditorFragment().updatePhoto(uri);
+                if (mIsPhotoSelection) {
+                    showEditorFragment();
+                }
+
+                // Re-create the photo handler the next time we need it so that additional photo
+                // selections create a new temp file (and don't hit the one that was just added
+                // to the cache).
+                mPhotoSelectionHandler = null;
+            }
+
+            @Override
+            public Uri getCurrentPhotoUri() {
+                return mPhotoUri;
+            }
+
+            @Override
+            public void onPhotoSelectionDismissed() {
+                if (mIsPhotoSelection) {
+                    showEditorFragment();
+                }
+            }
+        }
+
+        private final CompactPhotoActionListener mPhotoActionListener;
+        private final boolean mIsPhotoSelection;
+
+        public CompactPhotoSelectionHandler(int photoMode, boolean isPhotoSelection) {
+            // We pass a null changeAnchorView since we are overriding onClick so that we
+            // can show the photo options in a dialog instead of a ListPopupWindow (which would
+            // be anchored at changeAnchorView).
+
+            // TODO: empty raw contact delta list
+            super(CompactContactEditorActivity.this, /* changeAnchorView =*/ null, photoMode,
+                    /* isDirectoryContact =*/ false, new RawContactDeltaList());
+            mPhotoActionListener = new CompactPhotoActionListener();
+            mIsPhotoSelection = isPhotoSelection;
+        }
+
+        @Override
+        public PhotoActionListener getListener() {
+            return mPhotoActionListener;
+        }
+
+        @Override
+        protected void startPhotoActivity(Intent intent, int requestCode, Uri photoUri) {
+            mPhotoUri = photoUri;
+            startActivityForResult(intent, requestCode);
+        }
+    }
+
+    private CompactPhotoSelectionFragment mPhotoSelectionFragment;
+    private CompactPhotoSelectionHandler mPhotoSelectionHandler;
+    private Uri mPhotoUri;
+    private int mPhotoMode;
+    private boolean mIsPhotoSelection;
 
     @Override
     public void onCreate(Bundle savedState) {
@@ -41,19 +132,131 @@
 
         setContentView(R.layout.compact_contact_editor_activity);
 
-        mFragment = (CompactContactEditorFragment) getFragmentManager().findFragmentByTag(
-                TAG_COMPACT_EDITOR);
-        if (mFragment == null) {
+        if (savedState == null) {
+            // Create the editor and photo selection fragments
             mFragment = new CompactContactEditorFragment();
+            mPhotoSelectionFragment = new CompactPhotoSelectionFragment();
             getFragmentManager().beginTransaction()
-                    .add(R.id.compact_contact_editor_fragment_container,
-                            (CompactContactEditorFragment) mFragment, TAG_COMPACT_EDITOR)
+                    .add(R.id.fragment_container, getEditorFragment(), TAG_COMPACT_EDITOR)
+                    .add(R.id.fragment_container, mPhotoSelectionFragment, TAG_PHOTO_SELECTION)
+                    .hide(mPhotoSelectionFragment)
                     .commit();
-        }
-        mFragment.setListener(mFragmentListener);
+        } else {
+            // Restore state
+            mPhotoMode = savedState.getInt(STATE_PHOTO_MODE);
+            mIsPhotoSelection = savedState.getBoolean(STATE_IS_PHOTO_SELECTION);
 
+            // Show/hide the editor and photo selection fragments (w/o animations)
+            mFragment = (CompactContactEditorFragment) getFragmentManager()
+                    .findFragmentByTag(TAG_COMPACT_EDITOR);
+            mPhotoSelectionFragment = (CompactPhotoSelectionFragment) getFragmentManager()
+                    .findFragmentByTag(TAG_PHOTO_SELECTION);
+            final FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
+            if (mIsPhotoSelection) {
+                fragmentTransaction.hide(getEditorFragment()).show(mPhotoSelectionFragment);
+            } else {
+                fragmentTransaction.show(getEditorFragment()).hide(mPhotoSelectionFragment);
+            }
+            fragmentTransaction.commit();
+        }
+
+        // Set listeners
+        mFragment.setListener(mFragmentListener);
+        mPhotoSelectionFragment.setListener(this);
+
+        // Load editor data (even if it's hidden)
         final String action = getIntent().getAction();
         final Uri uri = Intent.ACTION_EDIT.equals(action) ? getIntent().getData() : null;
         mFragment.load(action, uri, getIntent().getExtras());
     }
+
+    protected void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putInt(STATE_PHOTO_MODE,  mPhotoMode);
+        outState.putBoolean(STATE_IS_PHOTO_SELECTION, mIsPhotoSelection);
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        getPhotoSelectionHandler().handlePhotoActivityResult(requestCode, resultCode, data);
+    }
+
+    @Override
+    public void onBackPressed() {
+        if (mIsPhotoSelection) {
+            mIsPhotoSelection = false;
+            showEditorFragment();
+        } else {
+            super.onBackPressed();
+        }
+    }
+
+    /**
+     * Displays photos from all raw contacts, clicking one set it as the super primary photo.
+     */
+    public void selectPhoto(ArrayList<CompactPhotoSelectionFragment.Photo> photos, int photoMode) {
+        mPhotoMode = photoMode;
+        mIsPhotoSelection = true;
+        mPhotoSelectionFragment.setPhotos(photos, photoMode);
+        showPhotoSelectionFragment();
+    }
+
+    /**
+     * Opens a dialog showing options for the user to change their photo (take, choose, or remove
+     * photo).
+     */
+    public void changePhoto(int photoMode) {
+        mPhotoMode = photoMode;
+        mIsPhotoSelection = false;
+        PhotoSourceDialogFragment.show(this, mPhotoMode);
+    }
+
+    private void showPhotoSelectionFragment() {
+        getFragmentManager().beginTransaction()
+                .setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out)
+                .hide(getEditorFragment())
+                .show(mPhotoSelectionFragment)
+                .commit();
+    }
+
+    private void showEditorFragment() {
+        getFragmentManager().beginTransaction()
+                .setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out)
+                .hide(mPhotoSelectionFragment)
+                .show((CompactContactEditorFragment) mFragment)
+                .commit();
+    }
+
+    @Override
+    public void onRemovePictureChosen() {
+        getPhotoSelectionHandler().getListener().onRemovePictureChosen();
+    }
+
+    @Override
+    public void onTakePhotoChosen() {
+        getPhotoSelectionHandler().getListener().onTakePhotoChosen();
+    }
+
+    @Override
+    public void onPickFromGalleryChosen() {
+        getPhotoSelectionHandler().getListener().onPickFromGalleryChosen();
+    }
+
+    @Override
+    public void onPhotoSelected(CompactPhotoSelectionFragment.Photo photo) {
+        getEditorFragment().setPrimaryPhoto(photo);
+        showEditorFragment();
+    }
+
+    private PhotoSelectionHandler getPhotoSelectionHandler() {
+        if (mPhotoSelectionHandler == null) {
+            mPhotoSelectionHandler = new CompactPhotoSelectionHandler(
+                    mPhotoMode, mIsPhotoSelection);
+        }
+        return mPhotoSelectionHandler;
+    }
+
+    private CompactContactEditorFragment getEditorFragment() {
+        return (CompactContactEditorFragment) mFragment;
+    }
 }
diff --git a/src/com/android/contacts/editor/CompactContactEditorFragment.java b/src/com/android/contacts/editor/CompactContactEditorFragment.java
index 031805b..c3ecd57 100644
--- a/src/com/android/contacts/editor/CompactContactEditorFragment.java
+++ b/src/com/android/contacts/editor/CompactContactEditorFragment.java
@@ -43,101 +43,17 @@
 import android.widget.Toast;
 
 import java.io.FileNotFoundException;
+import java.util.ArrayList;
 
 /**
  * Contact editor with only the most important fields displayed initially.
  */
 public class CompactContactEditorFragment extends ContactEditorBaseFragment implements
-        CompactRawContactsEditorView.Listener, PhotoSourceDialogFragment.Listener {
+        CompactRawContactsEditorView.Listener, CompactPhotoEditorView.Listener {
 
-    private static final String KEY_PHOTO_URI = "photo_uri";
     private static final String KEY_PHOTO_RAW_CONTACT_ID = "photo_raw_contact_id";
     private static final String KEY_UPDATED_PHOTOS = "updated_photos";
 
-    /**
-     * Displays a PopupWindow with photo edit options.
-     */
-    final class PhotoHandler extends PhotoSelectionHandler implements View.OnClickListener {
-
-        /**
-         * Receiver of photo edit option callbacks.
-         */
-        private final class PhotoListener extends PhotoActionListener {
-
-            @Override
-            public void onRemovePictureChosen() {
-                getContent().setPhoto(/* bitmap =*/ null);
-                mUpdatedPhotos.remove(String.valueOf(mPhotoRawContactId));
-
-                // Update the mode so the options change if user clicks the photo again
-                mPhotoMode = getPhotoMode();
-            }
-
-            @Override
-            public void onPhotoSelected(Uri uri) throws FileNotFoundException {
-                final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(getActivity(), uri);
-                if (bitmap == null || bitmap.getHeight() <= 0 || bitmap.getWidth() <= 0) {
-                    Log.w(TAG, "Invalid photo selected");
-                }
-                getContent().setPhoto(bitmap);
-
-                // If a new photo was chosen but not yet saved,
-                // we need to update the UI immediately
-                mUpdatedPhotos.putParcelable(String.valueOf(mPhotoRawContactId), uri);
-                getContent().setFullSizePhoto(uri);
-
-                // Update the mode so the options change if user clicks the photo again
-                mPhotoMode = getPhotoMode();
-
-                // Re-create the photo handler so that any additional photo selections create a
-                // new temp file (and don't hit the one that was just added to the cache).
-                mPhotoHandler = createPhotoHandler();
-            }
-
-            @Override
-            public Uri getCurrentPhotoUri() {
-                return mPhotoUri;
-            }
-
-            @Override
-            public void onPhotoSelectionDismissed() {
-            }
-        }
-
-        private PhotoListener mPhotoListener;
-        private int mPhotoMode;
-
-        public PhotoHandler(Context context, int photoMode, RawContactDeltaList state) {
-            // We pass a null changeAnchorView since we are overriding onClick so that we
-            // can show the photo options in a dialog instead of a ListPopupWindow (which would
-            // be anchored at changeAnchorView).
-            super(context, /* changeAnchorView =*/ null, photoMode, /* isDirectoryContact =*/ false,
-                    state);
-            mPhotoListener = new PhotoListener();
-            mPhotoMode = photoMode;
-        }
-
-        @Override
-        public void onClick(View view) {
-            PhotoSourceDialogFragment.show(CompactContactEditorFragment.this, mPhotoMode);
-        }
-
-        @Override
-        public PhotoActionListener getListener() {
-            return mPhotoListener;
-        }
-
-        @Override
-        protected void startPhotoActivity(Intent intent, int requestCode, Uri photoUri) {
-            mPhotoUri = photoUri;
-            mStatus = Status.SUB_ACTIVITY;
-
-            CompactContactEditorFragment.this.startActivityForResult(intent, requestCode);
-        }
-    }
-
-    private PhotoHandler mPhotoHandler;
-    private Uri mPhotoUri;
     private long mPhotoRawContactId;
     private Bundle mUpdatedPhotos = new Bundle();
 
@@ -146,7 +62,6 @@
         super.onCreate(savedState);
 
         if (savedState != null) {
-            mPhotoUri = savedState.getParcelable(KEY_PHOTO_URI);
             mPhotoRawContactId = savedState.getLong(KEY_PHOTO_RAW_CONTACT_ID);
             mUpdatedPhotos = savedState.getParcelable(KEY_UPDATED_PHOTOS);
         }
@@ -164,25 +79,12 @@
 
     @Override
     public void onSaveInstanceState(Bundle outState) {
-        outState.putParcelable(KEY_PHOTO_URI, mPhotoUri);
         outState.putLong(KEY_PHOTO_RAW_CONTACT_ID, mPhotoRawContactId);
         outState.putParcelable(KEY_UPDATED_PHOTOS, mUpdatedPhotos);
         super.onSaveInstanceState(outState);
     }
 
     @Override
-    public void onActivityResult(int requestCode, int resultCode, Intent data) {
-        if (mStatus == Status.SUB_ACTIVITY) {
-            mStatus = Status.EDITING;
-        }
-        if (mPhotoHandler != null
-                && mPhotoHandler.handlePhotoActivityResult(requestCode, resultCode, data)) {
-            return;
-        }
-        super.onActivityResult(requestCode, resultCode, data);
-    }
-
-    @Override
     public void onStop() {
         super.onStop();
 
@@ -213,13 +115,14 @@
                 mHasNewContact, mIsUserProfile, mAccountWithDataSet);
 
         // Set up the photo widget
-        mPhotoHandler = createPhotoHandler();
+        editorView.setPhotoListener(this);
         mPhotoRawContactId = editorView.getPhotoRawContactId();
-        if (mUpdatedPhotos.containsKey(String.valueOf(mPhotoRawContactId))) {
-            editorView.setFullSizePhoto((Uri) mUpdatedPhotos.getParcelable(
-                    String.valueOf(mPhotoRawContactId)));
+        // If there is an updated full resolution photo apply it now, this will be the case if
+        // the user selects or takes a new photo, then rotates the device.
+        final Uri uri = (Uri) mUpdatedPhotos.get(String.valueOf(mPhotoRawContactId));
+        if (uri != null) {
+            editorView.setFullSizePhoto(uri);
         }
-        editorView.setPhotoHandler(mPhotoHandler);
 
         // The editor is ready now so make it visible
         editorView.setEnabled(isEnabled());
@@ -252,37 +155,6 @@
         return true;
     }
 
-    private PhotoHandler createPhotoHandler() {
-        return new PhotoHandler(getActivity(), getPhotoMode(), mState);
-    }
-
-    private int getPhotoMode() {
-        // To determine the options that are available to the user to update their photo
-        // (i.e. the photo mode), check if any of the writable raw contacts has a photo set
-        Integer photoMode = null;
-        boolean hasWritableAccountType = false;
-        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
-        for (RawContactDelta rawContactDelta : mState) {
-            if (!rawContactDelta.isVisible()) {
-                continue;
-            }
-            final AccountType accountType = rawContactDelta.getAccountType(accountTypes);
-            if (accountType.areContactsWritable()) {
-                hasWritableAccountType = true;
-                if (getContent().isWritablePhotoSet()) {
-                    photoMode = PhotoActionPopup.Modes.MULTIPLE_WRITE_ABLE_PHOTOS;
-                    break;
-                }
-            }
-        }
-        // If the mode was not set, base it on whether we saw a writable contact or not
-        if (photoMode == null) {
-            photoMode = hasWritableAccountType
-                    ? PhotoActionPopup.Modes.NO_PHOTO : PhotoActionPopup.Modes.READ_ONLY_PHOTO;
-        }
-        return photoMode;
-    }
-
     @Override
     protected View getAggregationAnchorView(long rawContactId) {
         return getContent().getAggregationAnchorView();
@@ -314,25 +186,24 @@
         mContext.startService(intent);
     }
 
-    @Override
-    public void onRemovePictureChosen() {
-        if (mPhotoHandler != null) {
-            mPhotoHandler.getListener().onRemovePictureChosen();
-        }
+    public void removePhoto() {
+        getContent().removePhoto();
+        mUpdatedPhotos.remove(String.valueOf(mPhotoRawContactId));
     }
 
-    @Override
-    public void onTakePhotoChosen() {
-        if (mPhotoHandler != null) {
-            mPhotoHandler.getListener().onTakePhotoChosen();
+    public void updatePhoto(Uri uri) throws FileNotFoundException {
+        final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(getActivity(), uri);
+        if (bitmap == null || bitmap.getHeight() <= 0 || bitmap.getWidth() <= 0) {
+            Toast.makeText(getContext(), R.string.contactPhotoSavedErrorToast,
+                    Toast.LENGTH_SHORT).show();
+            return;
         }
+        mUpdatedPhotos.putParcelable(String.valueOf(mPhotoRawContactId), uri);
+        getContent().updatePhoto(uri);
     }
 
-    @Override
-    public void onPickFromGalleryChosen() {
-        if (mPhotoHandler != null) {
-            mPhotoHandler.getListener().onPickFromGalleryChosen();
-        }
+    public void setPrimaryPhoto(CompactPhotoSelectionFragment.Photo photo) {
+        getContent().setPrimaryPhoto(photo);
     }
 
     @Override
@@ -370,6 +241,35 @@
         getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupsLoaderListener);
     }
 
+    @Override
+    public void onPhotoEditorViewClicked() {
+        if (isMultiAccountContact()) {
+            final ArrayList<CompactPhotoSelectionFragment.Photo> photos = getContent().getPhotos();
+            if (!photos.isEmpty()) {
+                // For aggregate contacts, the user may select a new super primary photo from among
+                // the (non-default) raw contact photos, or source a new photo from the ActionBar
+                getEditorActivity().selectPhoto(photos, getPhotoMode());
+                return;
+            }
+        }
+        // For contacts composed of a single writable raw contact, or no raw contacts have photos,
+        // clicking the photo view simply opens the source photo dialog
+        getEditorActivity().changePhoto(getPhotoMode());
+    }
+
+    private int getPhotoMode() {
+        if (getContent().isWritablePhotoSet()) {
+            return isMultiAccountContact()
+                    ? PhotoActionPopup.Modes.MULTIPLE_WRITE_ABLE_PHOTOS
+                    : PhotoActionPopup.Modes.WRITE_ABLE_PHOTO;
+        }
+        return PhotoActionPopup.Modes.NO_PHOTO;
+    }
+
+    private CompactContactEditorActivity getEditorActivity() {
+        return (CompactContactEditorActivity) getActivity();
+    }
+
     private CompactRawContactsEditorView getContent() {
         return (CompactRawContactsEditorView) mContent;
     }
diff --git a/src/com/android/contacts/editor/CompactPhotoEditorView.java b/src/com/android/contacts/editor/CompactPhotoEditorView.java
index 783e044..1696a12 100644
--- a/src/com/android/contacts/editor/CompactPhotoEditorView.java
+++ b/src/com/android/contacts/editor/CompactPhotoEditorView.java
@@ -18,16 +18,8 @@
 
 import com.android.contacts.R;
 import com.android.contacts.common.ContactPhotoManager;
-import com.android.contacts.common.ContactPhotoManager.DefaultImageProvider;
-import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
-import com.android.contacts.common.ContactsUtils;
-import com.android.contacts.common.model.RawContactDelta;
 import com.android.contacts.common.model.ValuesDelta;
-import com.android.contacts.common.model.dataitem.DataKind;
-import com.android.contacts.common.util.MaterialColorMapUtils;
 import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette;
-import com.android.contacts.editor.CompactContactEditorFragment.PhotoHandler;
-import com.android.contacts.util.ContactPhotoUtils;
 import com.android.contacts.util.SchedulingUtils;
 import com.android.contacts.widget.QuickContactImageView;
 
@@ -35,30 +27,32 @@
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.drawable.GradientDrawable;
 import android.net.Uri;
 import android.provider.ContactsContract;
-import android.provider.ContactsContract.CommonDataKinds.Photo;
-import android.provider.ContactsContract.DisplayPhoto;
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
-import android.util.Log;
 import android.util.TypedValue;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.ImageView;
 import android.widget.RelativeLayout;
 
 /**
- * Displays the primary photo.
+ * Displays a photo and calls the host back when the user clicks it.
  */
 public class CompactPhotoEditorView extends RelativeLayout implements View.OnClickListener {
 
-    private static final String TAG = CompactContactEditorFragment.TAG;
+    /**
+     * Callbacks for the host of this view.
+     */
+    public interface Listener {
 
-    private ContactPhotoManager mContactPhotoManager;
-    private PhotoHandler mPhotoHandler;
+        /**
+         * Invoked when the user wants to change their photo.
+         */
+        void onPhotoEditorViewClicked();
+    }
+
+    private Listener mListener;
 
     private final float mLandscapePhotoRatio;
     private final float mPortraitPhotoRatio;
@@ -67,22 +61,21 @@
     private final int mActionBarHeight;
     private final int mStatusBarHeight;
 
-    private ValuesDelta mValuesDelta;
-    private boolean mReadOnly;
-    private boolean mIsPhotoSet;
-    private MaterialPalette mMaterialPalette;
-
     private QuickContactImageView mPhotoImageView;
     private View mPhotoIcon;
     private View mPhotoIconOverlay;
     private View mPhotoTouchInterceptOverlay;
 
+    private boolean mReadOnly;
+    private boolean mIsNonDefaultPhotoBound;
+
     public CompactPhotoEditorView(Context context) {
         this(context, null);
     }
 
     public CompactPhotoEditorView(Context context, AttributeSet attrs) {
         super(context, attrs);
+
         mLandscapePhotoRatio = getTypedFloat(R.dimen.quickcontact_landscape_photo_ratio);
         mPortraitPhotoRatio = getTypedFloat(R.dimen.editor_portrait_photo_ratio);
         mIsTwoPanel = getResources().getBoolean(R.bool.quickcontact_two_panel);
@@ -106,81 +99,56 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-        mContactPhotoManager = ContactPhotoManager.getInstance(getContext());
-
         mPhotoImageView = (QuickContactImageView) findViewById(R.id.photo);
         mPhotoIcon = findViewById(R.id.photo_icon);
         mPhotoIconOverlay = findViewById(R.id.photo_icon_overlay);
         mPhotoTouchInterceptOverlay = findViewById(R.id.photo_touch_intercept_overlay);
     }
 
-    public void setValues(DataKind dataKind, ValuesDelta valuesDelta,
-            RawContactDelta rawContactDelta, boolean readOnly, MaterialPalette materialPalette,
-            ViewIdGenerator viewIdGenerator) {
-        mValuesDelta = valuesDelta;
-        mReadOnly = readOnly;
-        mMaterialPalette = materialPalette;
+    public void setListener(Listener listener) {
+        mListener = listener;
+    }
 
+    public void setReadOnly(boolean readOnly) {
+        mReadOnly = readOnly;
         if (mReadOnly) {
             mPhotoIcon.setVisibility(View.GONE);
             mPhotoIconOverlay.setVisibility(View.GONE);
         } else {
             mPhotoTouchInterceptOverlay.setOnClickListener(this);
         }
-
-        setId(viewIdGenerator.getId(rawContactDelta, dataKind, valuesDelta, /* viewIndex =*/ 0));
-
-        setPhoto(valuesDelta);
     }
 
     /**
-     * Sets the photo bitmap on this view from the given ValuesDelta. Note that the
-     * RawContactDelta underlying this view is not modified in any way.  Using this method allows
-     * you to show one photo (from a read-only contact, for example) and yet have a different
-     * raw contact updated when a new photo is set (from the new raw contact created and attached
-     * to the read-only contact).
+     * Tries to bind a full size photo or a bitmap loaded from the given ValuesDelta,
+     * and falls back to the default avatar, tinted using the given MaterialPalette (if it's not
+     * null);
      */
-    public void setPhoto(ValuesDelta valuesDelta) {
-        if (valuesDelta == null) {
-            setDefaultPhoto();
-        } else {
-            final byte[] bytes = valuesDelta.getAsByteArray(Photo.PHOTO);
-            if (bytes == null) {
-                setDefaultPhoto();
-            } else {
-                final Bitmap bitmap = BitmapFactory.decodeByteArray(
-                        bytes, /* offset =*/ 0, bytes.length);
-                mPhotoImageView.setImageBitmap(bitmap);
-                mIsPhotoSet = true;
-                mValuesDelta.setFromTemplate(false);
-
-                // Check if we can update to the full size photo immediately
-                if (valuesDelta.getAfter() == null
-                        || valuesDelta.getAfter().get(Photo.PHOTO) == null) {
-                    // If the user hasn't updated the PHOTO value, then PHOTO_FILE_ID may contain
-                    // a reference to a larger version of PHOTO that we can bind to the UI.
-                    // Otherwise, we need to wait for a call to #setFullSizedPhoto() to update
-                    // our full sized image.
-                    final Long fileId = valuesDelta.getAsLong(Photo.PHOTO_FILE_ID);
-                    if (fileId != null) {
-                        final Uri photoUri = DisplayPhoto.CONTENT_URI.buildUpon()
-                                .appendPath(fileId.toString()).build();
-                        setFullSizedPhoto(photoUri);
-                    }
-                }
-            }
+    public void setPhoto(ValuesDelta valuesDelta, MaterialPalette materialPalette) {
+        // Check if we can update to the full size photo immediately
+        final Long photoFileId = EditorUiUtils.getPhotoFileId(valuesDelta);
+        if (photoFileId != null) {
+            final Uri photoUri = ContactsContract.DisplayPhoto.CONTENT_URI.buildUpon()
+                    .appendPath(photoFileId.toString()).build();
+            setFullSizedPhoto(photoUri);
+            adjustDimensions();
+            return;
         }
 
-        if (mIsPhotoSet) {
-            // Add background color behind the white photo icon so that it's visible even
-            // if the contact photo is white.
-            mPhotoIconOverlay.setBackground(new GradientDrawable(
-                    GradientDrawable.Orientation.TOP_BOTTOM, new int[]{0, 0x88000000}));
-        } else {
-            setDefaultPhotoTint();
+        // Use the bitmap image from the values delta
+        final Bitmap bitmap = EditorUiUtils.getPhotoBitmap(valuesDelta);
+        if (bitmap != null) {
+            setPhoto(bitmap);
+            adjustDimensions();
+            return;
         }
 
-        // Adjust the photo dimensions following the same logic as MultiShrinkScroll.initialize
+        setDefaultPhoto(materialPalette);
+        adjustDimensions();
+    }
+
+    private void adjustDimensions() {
+        // Follow the same logic as MultiShrinkScroll.initialize
         SchedulingUtils.doOnPreDraw(this, /* drawNextFrame =*/ false, new Runnable() {
             @Override
             public void run() {
@@ -190,7 +158,7 @@
                     photoWidth = (int) (photoHeight * mLandscapePhotoRatio);
                 } else {
                     // Make the photo slightly shorter that it is wide
-                    photoWidth = getWidth();
+                    photoWidth = getContentViewWidth();
                     photoHeight = (int) (photoWidth / mPortraitPhotoRatio);
                 }
                 final ViewGroup.LayoutParams layoutParams = getLayoutParams();
@@ -201,6 +169,13 @@
         });
     }
 
+    private int getContentViewWidth() {
+        final Activity activity = (Activity) getContext();
+        final DisplayMetrics displayMetrics = new DisplayMetrics();
+        activity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
+        return displayMetrics.widthPixels;
+    }
+
     // We're calculating the height the hard way because using the height of the content view
     // (found using android.view.Window.ID_ANDROID_CONTENT) with the soft keyboard up when
     // going from portrait to landscape mode results in a very small height value.
@@ -213,100 +188,46 @@
     }
 
     /**
-     * Set the {@link PhotoHandler} to forward clicks (i.e. requests to edit the photo) to.
-     */
-    public void setPhotoHandler(PhotoHandler photoHandler) {
-        mPhotoHandler = photoHandler;
-    }
-
-    /**
-     * Whether a writable {@link Photo} has been set.
+     * Whether a removable, non-default photo is bound to this view.
      */
     public boolean isWritablePhotoSet() {
-        return mIsPhotoSet && !mReadOnly;
+        return !mReadOnly && mIsNonDefaultPhotoBound;
     }
 
     /**
-     * Set the given {@link Bitmap} as the photo in the underlying {@link ValuesDelta}
-     * and bind a thumbnail to the UI.
+     * Binds the given bitmap.
      */
-    public void setPhoto(Bitmap bitmap) {
-        if (mReadOnly) {
-            Log.w(TAG, "Attempted to set read only photo. Aborting");
-            return;
-        }
-        if (bitmap == null) {
-            mValuesDelta.put(ContactsContract.CommonDataKinds.Photo.PHOTO, (byte[]) null);
-            setDefaultPhoto();
-            return;
-        }
+    private void setPhoto(Bitmap bitmap) {
+        mPhotoImageView.setImageBitmap(bitmap);
+        mIsNonDefaultPhotoBound = true;
+    }
 
-        final int thumbnailSize = ContactsUtils.getThumbnailSize(getContext());
-        final Bitmap scaledBitmap = Bitmap.createScaledBitmap(
-                bitmap, thumbnailSize, thumbnailSize, /* filter =*/ false);
-
-        mPhotoImageView.setImageBitmap(scaledBitmap);
-        mIsPhotoSet = true;
-        mValuesDelta.setFromTemplate(false);
-
-        // When the user chooses a new photo mark it as super primary
-        mValuesDelta.setSuperPrimary(true);
-
-        // Even though high-res photos cannot be saved by passing them via
-        // an EntityDeltaList (since they cause the Bundle size limit to be
-        // exceeded), we still pass a low-res thumbnail. This simplifies
-        // code all over the place, because we don't have to test whether
-        // there is a change in EITHER the delta-list OR a changed photo...
-        // this way, there is always a change in the delta-list.
-        final byte[] compressed = ContactPhotoUtils.compressBitmap(scaledBitmap);
-        if (compressed != null) {
-            mValuesDelta.setPhoto(compressed);
-        }
+    private void setDefaultPhoto(MaterialPalette materialPalette) {
+        EditorUiUtils.setDefaultPhoto(mPhotoImageView, getResources(), materialPalette);
     }
 
     /**
-     * Show the default "add photo" place holder.
-     */
-    private void setDefaultPhoto() {
-        mPhotoImageView.setImageDrawable(ContactPhotoManager.getDefaultAvatarDrawableForContact(
-                getResources(), /* hires =*/ false, /* defaultImageRequest =*/ null));
-        setDefaultPhotoTint();
-        mIsPhotoSet = false;
-        mValuesDelta.setFromTemplate(true);
-    }
-
-    private void setDefaultPhotoTint() {
-        final int color = mMaterialPalette == null
-                ? MaterialColorMapUtils.getDefaultPrimaryAndSecondaryColors(
-                        getResources()).mPrimaryColor
-                : mMaterialPalette.mPrimaryColor;
-        mPhotoImageView.setTint(color);
-    }
-
-    /**
-     * Bind the photo at the given Uri to the UI but do not set the photo on the underlying
-     * {@link ValuesDelta}.
+     * Binds a full size photo loaded from the given Uri.
      */
     public void setFullSizedPhoto(Uri photoUri) {
-        if (photoUri != null) {
-            final DefaultImageProvider fallbackToPreviousImage = new DefaultImageProvider() {
-                @Override
-                public void applyDefaultImage(ImageView view, int extent, boolean darkTheme,
-                        DefaultImageRequest defaultImageRequest) {
-                    // Before we finish setting the full sized image, don't change the current
-                    // image that is set in any way.
-                }
-            };
-            mContactPhotoManager.loadPhoto(mPhotoImageView, photoUri,
-                    mPhotoImageView.getWidth(), /* darkTheme =*/ false, /* isCircular =*/ false,
-                    /* defaultImageRequest =*/ null, fallbackToPreviousImage);
-        }
+        EditorUiUtils.loadPhoto(ContactPhotoManager.getInstance(getContext()),
+                mPhotoImageView, photoUri);
+        mIsNonDefaultPhotoBound = true;
+    }
+
+    /**
+     * Removes the current bound photo bitmap.
+     */
+    public void removePhoto() {
+        mPhotoImageView.setImageBitmap(/* bitmap =*/ null);
+        mIsNonDefaultPhotoBound = false;
+        setDefaultPhoto(/* materialPalette =*/ null);
     }
 
     @Override
     public void onClick(View view) {
-        if (mPhotoHandler != null) {
-            mPhotoHandler.onClick(view);
+        if (mListener != null) {
+            mListener.onPhotoEditorViewClicked();
         }
     }
 }
diff --git a/src/com/android/contacts/editor/CompactPhotoSelectionFragment.java b/src/com/android/contacts/editor/CompactPhotoSelectionFragment.java
new file mode 100644
index 0000000..ce868f9
--- /dev/null
+++ b/src/com/android/contacts/editor/CompactPhotoSelectionFragment.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2015 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.editor;
+
+import com.android.contacts.R;
+import com.android.contacts.common.model.ValuesDelta;
+import com.android.contacts.common.model.account.AccountType;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.GridView;
+import android.widget.ImageView;
+
+import java.util.ArrayList;
+
+/**
+ * Displays {@link Photo}s in a grid and calls back the host when one is clicked.
+ */
+public class CompactPhotoSelectionFragment extends Fragment {
+
+    private static final String STATE_PHOTOS = "photos";
+    private static final String STATE_PHOTO_MODE = "photoMode";
+
+    /**
+     * Callbacks hosts this Fragment.
+     */
+    public interface Listener {
+
+        /**
+         * Invoked when the user wants to change their photo.
+         */
+        void onPhotoSelected(Photo photo);
+    }
+
+    /**
+     * Holds a photo {@link ValuesDelta} and {@link AccountType} information to draw
+     * an account type icon over it.
+     */
+    public static final class Photo implements Parcelable {
+
+        public static final Creator<Photo> CREATOR = new Creator<Photo>() {
+
+            public Photo createFromParcel(Parcel in) {
+                return new Photo(in);
+            }
+
+            public Photo[] newArray(int size) {
+                return new Photo[size];
+            }
+        };
+
+        public Photo() {
+        }
+
+        private Photo(Parcel source) {
+            readFromParcel(source);
+        }
+
+        // From AccountType, everything we need to display the account type icon
+        public int titleRes;
+        public int iconRes;
+        public String syncAdapterPackageName;
+
+        public ValuesDelta valuesDelta;
+
+        /**
+         * Whether the photo is being displayed for the aggregate contact.
+         * This may be because it is marked super primary or it is the one quick contacts picked
+         * randomly to display because none is marked super primary.
+         */
+        public boolean primary;
+
+        public long rawContactId;
+
+        /**
+         * Pointer back to the KindSectionDataList this photo came from.
+         * See {@link CompactRawContactsEditorView#getPhotos}
+         * See {@link CompactRawContactsEditorView#setPrimaryPhoto}
+         */
+        public int kindSectionDataListIndex = -1;
+        public int valuesDeltaListIndex = -1;
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(titleRes);
+            dest.writeInt(iconRes);
+            dest.writeString(syncAdapterPackageName);
+            dest.writeParcelable(valuesDelta, flags);
+            dest.writeInt(primary ? 1 : 0);
+            dest.writeInt(kindSectionDataListIndex);
+            dest.writeInt(valuesDeltaListIndex);
+        }
+
+        private void readFromParcel(Parcel source) {
+            final ClassLoader classLoader = getClass().getClassLoader();
+            titleRes = source.readInt();
+            iconRes = source.readInt();
+            syncAdapterPackageName = source.readString();
+            valuesDelta = source.readParcelable(classLoader);
+            primary = source.readInt() == 1;
+            kindSectionDataListIndex = source.readInt();
+            valuesDeltaListIndex = source.readInt();
+        }
+    }
+
+    private final class PhotoAdapter extends BaseAdapter {
+
+        private final Context mContext;
+        private final LayoutInflater mLayoutInflater;
+
+        public PhotoAdapter() {
+            mContext = getContext();
+            mLayoutInflater = LayoutInflater.from(mContext);
+        }
+
+        @Override
+        public int getCount() {
+            return mPhotos == null ? 0 : mPhotos.size();
+        }
+
+        @Override
+        public Object getItem(int index) {
+            return mPhotos == null ? null : mPhotos.get(index);
+        }
+
+        @Override
+        public long getItemId(int index) {
+            return index;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            if (mPhotos == null) return null;
+
+            final View photoItemView;
+            if (convertView == null) {
+                photoItemView = mLayoutInflater.inflate(
+                        R.layout.compact_photo_selection_item, /* root =*/ null);
+            } else {
+                photoItemView = convertView;
+            }
+
+            final Photo photo = mPhotos.get(position);
+
+            // Bind the photo
+            final ImageView imageView = (ImageView) photoItemView.findViewById(R.id.image);
+            imageView.setImageBitmap(EditorUiUtils.getPhotoBitmap(photo.valuesDelta));
+
+            // Add the account type icon
+            final ImageView accountTypeImageView = (ImageView)
+                    photoItemView.findViewById(R.id.account_type);
+            accountTypeImageView.setImageDrawable(AccountType.getDisplayIcon(
+                    mContext, photo.titleRes, photo.iconRes, photo.syncAdapterPackageName));
+
+            // Display a check icon over the primary photo
+            final ImageView checkImageView = (ImageView) photoItemView.findViewById(R.id.check);
+            checkImageView.setVisibility(photo.primary ? View.VISIBLE : View.GONE);
+
+            return photoItemView;
+        }
+    }
+
+    private ArrayList<Photo> mPhotos;
+    private int mPhotoMode;
+    private Listener mListener;
+
+    public void setListener(Listener listener) {
+        mListener = listener;
+    }
+
+    public void setPhotos(ArrayList<Photo> photos, int photoMode) {
+        mPhotos = photos;
+        mPhotoMode = photoMode;
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        if (savedInstanceState != null) {
+            mPhotos = savedInstanceState.getParcelableArrayList(STATE_PHOTOS);
+            mPhotoMode = savedInstanceState.getInt(STATE_PHOTO_MODE, 0);
+        }
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
+        setHasOptionsMenu(true);
+
+        final PhotoAdapter photoAdapter = new PhotoAdapter();
+
+        final View view = inflater.inflate(R.layout.compact_photo_selection_fragment,
+                container, false);
+        final GridView gridView = (GridView) view.findViewById(R.id.grid_view);
+        gridView.setAdapter(photoAdapter);
+        gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+                // Call the host back so it can set the new photo as primary
+                final Photo photo = (Photo) photoAdapter.getItem(position);
+                if (mListener != null) {
+                    mListener.onPhotoSelected(photo);
+                }
+            }
+        });
+
+        return view;
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        outState.putParcelableArrayList(STATE_PHOTOS, mPhotos);
+        outState.putInt(STATE_PHOTO_MODE, mPhotoMode);
+        super.onSaveInstanceState(outState);
+    }
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+        menuInflater.inflate(R.menu.edit_contact_photo, menu);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case android.R.id.home:
+                getActivity().onBackPressed();
+                return true;
+            case R.id.menu_photo:
+                PhotoSourceDialogFragment.show(getActivity(), mPhotoMode);
+                return true;
+            default:
+                return super.onOptionsItemSelected(item);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/editor/CompactRawContactsEditorView.java b/src/com/android/contacts/editor/CompactRawContactsEditorView.java
index e71c814..090ea31 100644
--- a/src/com/android/contacts/editor/CompactRawContactsEditorView.java
+++ b/src/com/android/contacts/editor/CompactRawContactsEditorView.java
@@ -17,6 +17,7 @@
 package com.android.contacts.editor;
 
 import com.android.contacts.R;
+import com.android.contacts.common.ContactsUtils;
 import com.android.contacts.common.model.AccountTypeManager;
 import com.android.contacts.common.model.RawContactDelta;
 import com.android.contacts.common.model.RawContactDeltaList;
@@ -27,7 +28,7 @@
 import com.android.contacts.common.model.dataitem.DataKind;
 import com.android.contacts.common.util.AccountsListAdapter;
 import com.android.contacts.common.util.MaterialColorMapUtils;
-import com.android.contacts.editor.CompactContactEditorFragment.PhotoHandler;
+import com.android.contacts.util.ContactPhotoUtils;
 import com.android.contacts.util.UiClosables;
 
 import android.content.Context;
@@ -62,6 +63,8 @@
 import android.widget.ListPopupWindow;
 import android.widget.TextView;
 
+import java.io.File;
+import java.io.FileNotFoundException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -321,6 +324,7 @@
 
     private boolean mIsExpanded;
     private long mPhotoRawContactId;
+    private ValuesDelta mPhotoValuesDelta;
 
     public CompactRawContactsEditorView(Context context) {
         super(context);
@@ -401,17 +405,17 @@
     }
 
     /**
-     * Pass through to {@link CompactPhotoEditorView#setPhotoHandler}.
+     * Pass through to {@link CompactPhotoEditorView#setListener}.
      */
-    public void setPhotoHandler(PhotoHandler photoHandler) {
-        mPhotoView.setPhotoHandler(photoHandler);
+    public void setPhotoListener(CompactPhotoEditorView.Listener listener) {
+        mPhotoView.setListener(listener);
     }
 
-    /**
-     * Pass through to {@link CompactPhotoEditorView#setPhoto}.
-     */
-    public void setPhoto(Bitmap bitmap) {
-        mPhotoView.setPhoto(bitmap);
+    public void removePhoto() {
+        mPhotoValuesDelta.setFromTemplate(false);
+        mPhotoValuesDelta.put(Photo.PHOTO, (byte[]) null);
+
+        mPhotoView.removePhoto();
     }
 
     /**
@@ -421,6 +425,28 @@
         mPhotoView.setFullSizedPhoto(photoUri);
     }
 
+    public void updatePhoto(Uri photoUri) {
+        // Even though high-res photos cannot be saved by passing them via
+        // an EntityDeltaList (since they cause the Bundle size limit to be
+        // exceeded), we still pass a low-res thumbnail. This simplifies
+        // code all over the place, because we don't have to test whether
+        // there is a change in EITHER the delta-list OR a changed photo...
+        // this way, there is always a change in the delta-list.
+        mPhotoValuesDelta.setFromTemplate(false);
+        mPhotoValuesDelta.setSuperPrimary(true);
+        try {
+            final byte[] bytes = EditorUiUtils.getCompressedThumbnailBitmapBytes(
+                    getContext(), photoUri);
+            if (bytes != null) {
+                mPhotoValuesDelta.setPhoto(bytes);
+            }
+        } catch (FileNotFoundException e) {
+            elog("Failed to get bitmap from photo Uri");
+        }
+
+        mPhotoView.setFullSizedPhoto(photoUri);
+    }
+
     /**
      * Pass through to {@link CompactPhotoEditorView#isWritablePhotoSet}.
      */
@@ -435,6 +461,77 @@
         return mPhotoRawContactId;
     }
 
+    /**
+     * Returns a data holder for every non-default/non-empty photo from each raw contact, whether
+     * the raw contact is writable or not.
+     */
+    public ArrayList<CompactPhotoSelectionFragment.Photo> getPhotos() {
+        final ArrayList<CompactPhotoSelectionFragment.Photo> photos = new ArrayList<>();
+
+        final List<KindSectionData> kindSectionDataList =
+                mKindSectionDataMap.get(Photo.CONTENT_ITEM_TYPE);
+        for (int i = 0; i < kindSectionDataList.size(); i++) {
+            final KindSectionData kindSectionData = kindSectionDataList.get(i);
+            final AccountType accountType = kindSectionData.getAccountType();
+            final List<ValuesDelta> valuesDeltaList = kindSectionData.getValuesDeltas();
+            if (valuesDeltaList == null || valuesDeltaList.isEmpty()) continue;
+            for (int j = 0; j < valuesDeltaList.size(); j++) {
+                final ValuesDelta valuesDelta = valuesDeltaList.get(j);
+                final Bitmap bitmap = EditorUiUtils.getPhotoBitmap(valuesDelta);
+                if (bitmap == null) continue;
+
+                final CompactPhotoSelectionFragment.Photo photo =
+                        new CompactPhotoSelectionFragment.Photo();
+                photo.titleRes = accountType.titleRes;
+                photo.iconRes = accountType.iconRes;
+                photo.syncAdapterPackageName = accountType.syncAdapterPackageName;
+                photo.valuesDelta = valuesDelta;
+                photo.primary = valuesDelta.isSuperPrimary();
+                photo.kindSectionDataListIndex = i;
+                photo.valuesDeltaListIndex = j;
+                photos.add(photo);
+            }
+        }
+
+        return photos;
+    }
+
+    /**
+     * Marks the raw contact photo given as primary for the aggregate contact and updates the
+     * UI.
+     */
+    public void setPrimaryPhoto(CompactPhotoSelectionFragment.Photo photo) {
+        // Unset primary for all other photos
+        final List<KindSectionData> kindSectionDataList =
+                mKindSectionDataMap.get(Photo.CONTENT_ITEM_TYPE);
+        for (KindSectionData kindSectionData : kindSectionDataList) {
+            final List<ValuesDelta> valuesDeltaList = kindSectionData.getValuesDeltas();
+            for (ValuesDelta valuesDelta : valuesDeltaList) {
+                valuesDelta.setSuperPrimary(false);
+            }
+        }
+
+        // Find the values delta to mark as primary
+        if (photo.kindSectionDataListIndex < 0
+                || photo.kindSectionDataListIndex >= kindSectionDataList.size()) {
+            wlog("Invalid kind section data list index");
+            return;
+        }
+        final KindSectionData kindSectionData =
+                kindSectionDataList.get(photo.kindSectionDataListIndex);
+        final List<ValuesDelta> valuesDeltaList = kindSectionData.getValuesDeltas();
+        if (photo.valuesDeltaListIndex >= valuesDeltaList.size()) {
+            wlog("Invalid values delta list index");
+            return;
+        }
+        final ValuesDelta valuesDelta = valuesDeltaList.get(photo.valuesDeltaListIndex);
+        valuesDelta.setFromTemplate(false);
+        valuesDelta.setSuperPrimary(true);
+
+        // Update the UI
+        mPhotoView.setPhoto(valuesDelta, mMaterialPalette);
+    }
+
     public View getAggregationAnchorView() {
         final List<CompactKindSectionView> kindSectionViews = getKindSectionViews(
                 StructuredName.CONTENT_ITEM_TYPE);
@@ -691,31 +788,50 @@
     }
 
     private void addPhotoView() {
-        // Get the kind section data and values delta that will back the photo view
-        Pair<KindSectionData,ValuesDelta> pair = getPrimaryKindSectionData(mPhotoId);
+        // Get the kind section data and values delta that we will display in the photo view
+        Pair<KindSectionData,ValuesDelta> pair = getPrimaryPhotoKindSectionData(mPhotoId);
         if (pair == null) {
             wlog("photo: no kind section data parsed");
+            mPhotoView.setReadOnly(true);
             return;
         }
-        final KindSectionData kindSectionData = pair.first;
-        final ValuesDelta valuesDelta = pair.second;
 
-        // If we're editing a read-only contact we want to display the photo from the
-        // read-only contact in a photo editor backed by the new raw contact
-        // that was created.
-        if (mHasNewContact) {
-            mPhotoRawContactId = mPrimaryRawContactDelta == null
-                    ? null : mPrimaryRawContactDelta.getRawContactId();
+        // Set the photo view
+        final ValuesDelta primaryValuesDelta = pair.second;
+        mPhotoView.setPhoto(primaryValuesDelta, mMaterialPalette);
+
+        // Find the raw contact ID and values delta that will be written when the photo is edited
+        final KindSectionData primaryKindSectionData = pair.first;
+        if (mHasNewContact && mPrimaryRawContactDelta != null
+                && !primaryKindSectionData.getValuesDeltas().isEmpty()) {
+            // If we're editing a read-only contact we want to display the photo from the
+            // read-only contact in a photo editor view, but update the new raw contact
+            // that was created.
+            mPhotoRawContactId = mPrimaryRawContactDelta.getRawContactId();
+            mPhotoValuesDelta = primaryKindSectionData.getValuesDeltas().get(0);
+            mPhotoView.setReadOnly(false);
+            return;
+        }
+        if (primaryKindSectionData.getAccountType().areContactsWritable() &&
+                !primaryKindSectionData.getValuesDeltas().isEmpty()) {
+            mPhotoRawContactId = primaryKindSectionData.getRawContactDelta().getRawContactId();
+            mPhotoValuesDelta = primaryKindSectionData.getValuesDeltas().get(0);
+            mPhotoView.setReadOnly(false);
+            return;
         }
 
-        mPhotoRawContactId = kindSectionData.getRawContactDelta().getRawContactId();
-        mPhotoView.setValues(kindSectionData.getDataKind(), valuesDelta,
-                kindSectionData.getRawContactDelta(),
-                !kindSectionData.getAccountType().areContactsWritable(), mMaterialPalette,
-                mViewIdGenerator);
+        final KindSectionData writableKindSectionData = getFirstWritablePhotoKindSectionData();
+        if (writableKindSectionData == null
+                || writableKindSectionData.getValuesDeltas().isEmpty()) {
+            mPhotoView.setReadOnly(true);
+            return;
+        }
+        mPhotoRawContactId = writableKindSectionData.getRawContactDelta().getRawContactId();
+        mPhotoValuesDelta = writableKindSectionData.getValuesDeltas().get(0);
+        mPhotoView.setReadOnly(false);
     }
 
-    private Pair<KindSectionData,ValuesDelta> getPrimaryKindSectionData(long id) {
+    private Pair<KindSectionData,ValuesDelta> getPrimaryPhotoKindSectionData(long id) {
         final String mimeType = Photo.CONTENT_ITEM_TYPE;
         final List<KindSectionData> kindSectionDataList = mKindSectionDataMap.get(mimeType);
 
@@ -766,6 +882,17 @@
                 ? new Pair<>(resultKindSectionData, resultValuesDelta) : null;
     }
 
+    private KindSectionData getFirstWritablePhotoKindSectionData() {
+        final String mimeType = Photo.CONTENT_ITEM_TYPE;
+        final List<KindSectionData> kindSectionDataList = mKindSectionDataMap.get(mimeType);
+        for (KindSectionData kindSectionData : kindSectionDataList) {
+            if (kindSectionData.getAccountType().areContactsWritable()) {
+                return kindSectionData;
+            }
+        }
+        return null;
+    }
+
     private void addKindSectionViews() {
         // Sort the kinds
         final TreeSet<Map.Entry<String,List<KindSectionData>>> entries =
diff --git a/src/com/android/contacts/editor/ContactEditorBaseFragment.java b/src/com/android/contacts/editor/ContactEditorBaseFragment.java
index 2c3b6c9..c5e6afd 100644
--- a/src/com/android/contacts/editor/ContactEditorBaseFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorBaseFragment.java
@@ -743,7 +743,7 @@
             // even if they have never added their own information and splitting will create a
             // name only contact.
             final boolean isSingleReadOnlyContact = mHasNewContact && mState.size() == 2;
-            splitMenu.setVisible(mState.size() > 1 && !isEditingUserProfile()
+            splitMenu.setVisible(isMultiAccountContact() && !isEditingUserProfile()
                     && !isSingleReadOnlyContact);
             // Cannot join a user profile
             joinMenu.setVisible(!isEditingUserProfile());
@@ -938,6 +938,10 @@
         return mNewLocalProfile || mIsUserProfile;
     }
 
+    protected boolean isMultiAccountContact() {
+        return mState.size() > 1;
+    }
+
     /**
      * Return true if there are any edits to the current contact which need to
      * be saved.
diff --git a/src/com/android/contacts/editor/EditorUiUtils.java b/src/com/android/contacts/editor/EditorUiUtils.java
index 106c5e3..0fdae58 100644
--- a/src/com/android/contacts/editor/EditorUiUtils.java
+++ b/src/com/android/contacts/editor/EditorUiUtils.java
@@ -18,8 +18,12 @@
 
 import static android.provider.ContactsContract.CommonDataKinds.GroupMembership;
 import static android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import static com.android.contacts.common.util.MaterialColorMapUtils.getDefaultPrimaryAndSecondaryColors;
 
 import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
 import android.graphics.drawable.Drawable;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.Event;
@@ -37,12 +41,24 @@
 import android.os.Build;
 import android.text.TextUtils;
 import android.util.Pair;
+import android.widget.ImageView;
+
 import com.android.contacts.R;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.ContactPhotoManager.DefaultImageProvider;
+import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.model.ValuesDelta;
 import com.android.contacts.common.model.account.AccountType;
 import com.android.contacts.common.model.account.GoogleAccountType;
 import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette;
+import com.android.contacts.util.ContactPhotoUtils;
+import com.android.contacts.widget.QuickContactImageView;
+
 import com.google.common.collect.Maps;
 
+import java.io.FileNotFoundException;
 import java.util.HashMap;
 
 /**
@@ -219,4 +235,62 @@
     private static boolean isNewerThanM(int currentVersion) {
         return currentVersion > Build.VERSION_CODES.M;
     }
+
+    /** Returns teh {@link Photo#PHOTO_FILE_ID} from the given ValuesDelta. */
+    public static Long getPhotoFileId(ValuesDelta valuesDelta) {
+        if (valuesDelta == null) return null;
+        if (valuesDelta.getAfter() == null || valuesDelta.getAfter().get(Photo.PHOTO) == null) {
+            return valuesDelta.getAsLong(Photo.PHOTO_FILE_ID);
+        }
+        return null;
+    }
+
+    /** Binds the full resolution image at the given Uri to the provided ImageView. */
+    static void loadPhoto(ContactPhotoManager contactPhotoManager, ImageView imageView,
+            Uri photoUri) {
+        final DefaultImageProvider fallbackToPreviousImage = new DefaultImageProvider() {
+            @Override
+            public void applyDefaultImage(ImageView view, int extent, boolean darkTheme,
+                    DefaultImageRequest defaultImageRequest) {
+                // Before we finish setting the full sized image, don't change the current
+                // image that is set in any way.
+            }
+        };
+        contactPhotoManager.loadPhoto(imageView, photoUri, imageView.getWidth(),
+                /* darkTheme =*/ false, /* isCircular =*/ false,
+                /* defaultImageRequest =*/ null, fallbackToPreviousImage);
+    }
+
+    /** Decodes the Bitmap from the photo bytes from the given ValuesDelta. */
+    public static Bitmap getPhotoBitmap(ValuesDelta valuesDelta) {
+        if (valuesDelta == null) return null;
+        final byte[] bytes = valuesDelta.getAsByteArray(Photo.PHOTO);
+        if (bytes == null) return null;
+        return BitmapFactory.decodeByteArray(bytes, /* offset =*/ 0, bytes.length);
+    }
+
+    /** Binds the default avatar to the given ImageView and tints it to match QuickContacts. */
+    public static void setDefaultPhoto(ImageView imageView , Resources resources,
+            MaterialPalette materialPalette) {
+        // Use the default avatar drawable
+        imageView.setImageDrawable(ContactPhotoManager.getDefaultAvatarDrawableForContact(
+                resources, /* hires =*/ false, /* defaultImageRequest =*/ null));
+
+        // Tint it to match the quick contacts
+        if (imageView instanceof QuickContactImageView) {
+            ((QuickContactImageView) imageView).setTint(materialPalette == null
+                    ? getDefaultPrimaryAndSecondaryColors(resources).mPrimaryColor
+                    : materialPalette.mPrimaryColor);
+        }
+    }
+
+    /**  Returns compressed bitmap bytes from the given Uri, scaled to the thumbnail dimensions. */
+    public static byte[] getCompressedThumbnailBitmapBytes(Context context, Uri uri)
+            throws FileNotFoundException {
+        final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(context, uri);
+        final int size = ContactsUtils.getThumbnailSize(context);
+        final Bitmap bitmapScaled = Bitmap.createScaledBitmap(
+                bitmap, size, size, /* filter =*/ false);
+        return ContactPhotoUtils.compressBitmap(bitmapScaled);
+    }
 }
diff --git a/src/com/android/contacts/editor/KindSectionData.java b/src/com/android/contacts/editor/KindSectionData.java
index 33aeed2..6c33601 100644
--- a/src/com/android/contacts/editor/KindSectionData.java
+++ b/src/com/android/contacts/editor/KindSectionData.java
@@ -27,7 +27,6 @@
 import android.provider.ContactsContract.CommonDataKinds.Event;
 import android.text.TextUtils;
 
-import java.util.Collections;
 import java.util.List;
 
 /**
diff --git a/src/com/android/contacts/editor/PhotoSourceDialogFragment.java b/src/com/android/contacts/editor/PhotoSourceDialogFragment.java
index e16fdae..4b502e5 100644
--- a/src/com/android/contacts/editor/PhotoSourceDialogFragment.java
+++ b/src/com/android/contacts/editor/PhotoSourceDialogFragment.java
@@ -19,6 +19,7 @@
 import com.android.contacts.R;
 import com.android.contacts.editor.PhotoActionPopup.ChoiceListItem;
 
+import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.app.DialogFragment;
@@ -44,14 +45,17 @@
         void onPickFromGalleryChosen();
     }
 
-    public static void show(CompactContactEditorFragment fragment, int photoMode) {
+    public static void show(Activity activity, int photoMode) {
+        if (!(activity instanceof Listener)) {
+            throw new IllegalArgumentException(
+                    "Activity must implement " + Listener.class.getName());
+        }
         final Bundle args = new Bundle();
         args.putInt(ARG_PHOTO_MODE, photoMode);
 
         PhotoSourceDialogFragment dialog = new PhotoSourceDialogFragment();
-        dialog.setTargetFragment(fragment, 0);
         dialog.setArguments(args);
-        dialog.show(fragment.getFragmentManager(), "photoSource");
+        dialog.show(activity.getFragmentManager(), "photoSource");
     }
 
     @Override
@@ -69,7 +73,7 @@
         final OnClickListener clickListener = new OnClickListener() {
             @Override
             public void onClick(DialogInterface dialogInterface, int which) {
-                final Listener listener = (Listener) getTargetFragment();
+                final Listener listener = (Listener) getActivity();
                 final ChoiceListItem choice = choices.get(which);
                 switch (choice.getId()) {
                     case ChoiceListItem.ID_REMOVE:
diff --git a/src/com/android/contacts/list/MultiSelectContactsListFragment.java b/src/com/android/contacts/list/MultiSelectContactsListFragment.java
index bbb0d84..4d2eae8 100644
--- a/src/com/android/contacts/list/MultiSelectContactsListFragment.java
+++ b/src/com/android/contacts/list/MultiSelectContactsListFragment.java
@@ -25,6 +25,7 @@
 import android.os.Bundle;
 import android.provider.ContactsContract;
 import android.text.TextUtils;
+import android.view.accessibility.AccessibilityEvent;
 
 import java.util.TreeSet;
 
@@ -125,6 +126,14 @@
                     mCheckBoxListListener.onStartDisplayingCheckBoxes();
                 }
                 getAdapter().toggleSelectionOfContactId(Long.valueOf(contactId));
+                // Manually send clicked event if there is a checkbox.
+                // See b/24098561.  TalkBack will not read it otherwise.
+                final int index = position + getListView().getHeaderViewsCount() - getListView()
+                        .getFirstVisiblePosition();
+                if (index >= 0 && index < getListView().getChildCount()) {
+                    getListView().getChildAt(index).sendAccessibilityEvent(AccessibilityEvent
+                            .TYPE_VIEW_CLICKED);
+                }
             }
         }
         final int nowSelectedCount = getAdapter().getSelectedContactIds().size();