Merge "Fix regression; once again we can apply Gallery photo to contact."
diff --git a/src/com/android/contacts/ContactLoader.java b/src/com/android/contacts/ContactLoader.java
index 17cd1e7..c0399e4 100644
--- a/src/com/android/contacts/ContactLoader.java
+++ b/src/com/android/contacts/ContactLoader.java
@@ -19,6 +19,7 @@
 import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
 import com.android.contacts.model.AccountTypeWithDataSet;
+import com.android.contacts.model.EntityDeltaList;
 import com.android.contacts.util.ContactLoaderUtils;
 import com.android.contacts.util.DataStatus;
 import com.android.contacts.util.StreamItemEntry;
@@ -71,7 +72,7 @@
  * Loads a single Contact and all it constituent RawContacts.
  */
 public class ContactLoader extends AsyncTaskLoader<ContactLoader.Result> {
-    private static final String TAG = "ContactLoader";
+    private static final String TAG = ContactLoader.class.getSimpleName();
 
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
@@ -313,6 +314,13 @@
         }
 
         /**
+         * Instantiate a new EntityDeltaList for this contact.
+         */
+        public EntityDeltaList createEntityDeltaList() {
+            return EntityDeltaList.fromIterator(getEntities().iterator());
+        }
+
+        /**
          * Returns the contact ID.
          */
         @VisibleForTesting
@@ -419,16 +427,31 @@
          *         writable raw-contact, and false otherwise.
          */
         public boolean isWritableContact(final Context context) {
-            if (isDirectoryEntry()) return false;
+            return getFirstWritableRawContactId(context) != -1;
+        }
+
+        /**
+         * Return the ID of the first raw-contact in the contact data that belongs to a
+         * contact-writable account, or -1 if no such entity exists.
+         */
+        public long getFirstWritableRawContactId(final Context context) {
+            // Directory entries are non-writable
+            if (isDirectoryEntry()) return -1;
+
+            // Iterate through raw-contacts; if we find a writable on, return its ID.
             final AccountTypeManager accountTypes = AccountTypeManager.getInstance(context);
-            for (Entity rawContact : getEntities()) {
-                final ContentValues rawValues = rawContact.getEntityValues();
-                final String accountType = rawValues.getAsString(RawContacts.ACCOUNT_TYPE);
-                final String dataSet = rawValues.getAsString(RawContacts.DATA_SET);
-                final AccountType type = accountTypes.getAccountType(accountType, dataSet);
-                if (type != null && type.areContactsWritable()) return true;
+            for (Entity entity : getEntities()) {
+                ContentValues values = entity.getEntityValues();
+                String type = values.getAsString(RawContacts.ACCOUNT_TYPE);
+                String dataSet = values.getAsString(RawContacts.DATA_SET);
+
+                AccountType accountType = accountTypes.getAccountType(type, dataSet);
+                if (accountType != null && accountType.areContactsWritable()) {
+                    return values.getAsLong(RawContacts._ID);
+                }
             }
-            return false;
+            // No writable raw-contact was found.
+            return -1;
         }
 
         public int getDirectoryExportSupport() {
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index 3fbca54..fdfd0f7 100644
--- a/src/com/android/contacts/ContactSaveService.java
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -222,7 +222,7 @@
      */
     public static Intent createNewRawContactIntent(Context context,
             ArrayList<ContentValues> values, AccountWithDataSet account,
-            Class<?> callbackActivity, String callbackAction) {
+            Class<? extends Activity> callbackActivity, String callbackAction) {
         Intent serviceIntent = new Intent(
                 context, ContactSaveService.class);
         serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
@@ -290,8 +290,9 @@
      * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
      */
     public static Intent createSaveContactIntent(Context context, EntityDeltaList state,
-            String saveModeExtraKey, int saveMode, boolean isProfile, Class<?> callbackActivity,
-            String callbackAction, long rawContactId, String updatedPhotoPath) {
+            String saveModeExtraKey, int saveMode, boolean isProfile,
+            Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
+            String updatedPhotoPath) {
         Bundle bundle = new Bundle();
         bundle.putString(String.valueOf(rawContactId), updatedPhotoPath);
         return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
@@ -306,8 +307,9 @@
      * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
      */
     public static Intent createSaveContactIntent(Context context, EntityDeltaList state,
-            String saveModeExtraKey, int saveMode, boolean isProfile, Class<?> callbackActivity,
-            String callbackAction, Bundle updatedPhotos) {
+            String saveModeExtraKey, int saveMode, boolean isProfile,
+            Class<? extends Activity> callbackActivity, String callbackAction,
+            Bundle updatedPhotos) {
         Intent serviceIntent = new Intent(
                 context, ContactSaveService.class);
         serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
@@ -317,19 +319,20 @@
             serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
         }
 
-        // Callback intent will be invoked by the service once the contact is
-        // saved.  The service will put the URI of the new contact as "data" on
-        // the callback intent.
-        Intent callbackIntent = new Intent(context, callbackActivity);
-        callbackIntent.putExtra(saveModeExtraKey, saveMode);
-        callbackIntent.setAction(callbackAction);
-        serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
+        if (callbackActivity != null) {
+            // Callback intent will be invoked by the service once the contact is
+            // saved.  The service will put the URI of the new contact as "data" on
+            // the callback intent.
+            Intent callbackIntent = new Intent(context, callbackActivity);
+            callbackIntent.putExtra(saveModeExtraKey, saveMode);
+            callbackIntent.setAction(callbackAction);
+            serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
+        }
         return serviceIntent;
     }
 
     private void saveContact(Intent intent) {
         EntityDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
-        Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
         boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
         Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
 
@@ -462,15 +465,17 @@
             }
         }
 
-        if (succeeded) {
-            // Mark the intent to indicate that the save was successful (even if the lookup URI
-            // is now null).  For local contacts or the local profile, it's possible that the
-            // save triggered removal of the contact, so no lookup URI would exist..
-            callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
+        Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
+        if (callbackIntent != null) {
+            if (succeeded) {
+                // Mark the intent to indicate that the save was successful (even if the lookup URI
+                // is now null).  For local contacts or the local profile, it's possible that the
+                // save triggered removal of the contact, so no lookup URI would exist..
+                callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
+            }
+            callbackIntent.setData(lookupUri);
+            deliverCallback(callbackIntent);
         }
-        callbackIntent.setData(lookupUri);
-
-        deliverCallback(callbackIntent);
     }
 
     /**
@@ -554,7 +559,7 @@
      * @param callbackAction is the intent action for the callback intent
      */
     public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
-            String label, long[] rawContactsToAdd, Class<?> callbackActivity,
+            String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
             String callbackAction) {
         Intent serviceIntent = new Intent(context, ContactSaveService.class);
         serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
@@ -618,7 +623,7 @@
      * Creates an intent that can be sent to this service to rename a group.
      */
     public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
-            Class<?> callbackActivity, String callbackAction) {
+            Class<? extends Activity> callbackActivity, String callbackAction) {
         Intent serviceIntent = new Intent(context, ContactSaveService.class);
         serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
         serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
@@ -689,7 +694,7 @@
      */
     public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
             long[] rawContactsToAdd, long[] rawContactsToRemove,
-            Class<?> callbackActivity, String callbackAction) {
+            Class<? extends Activity> callbackActivity, String callbackAction) {
         Intent serviceIntent = new Intent(context, ContactSaveService.class);
         serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
         serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
@@ -959,7 +964,7 @@
      */
     public static Intent createJoinContactsIntent(Context context, long contactId1,
             long contactId2, boolean contactWritable,
-            Class<?> callbackActivity, String callbackAction) {
+            Class<? extends Activity> callbackActivity, String callbackAction) {
         Intent serviceIntent = new Intent(context, ContactSaveService.class);
         serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
diff --git a/src/com/android/contacts/activities/AttachPhotoActivity.java b/src/com/android/contacts/activities/AttachPhotoActivity.java
index 8d4cb5d..5981e9b 100644
--- a/src/com/android/contacts/activities/AttachPhotoActivity.java
+++ b/src/com/android/contacts/activities/AttachPhotoActivity.java
@@ -18,29 +18,31 @@
 
 import com.android.contacts.ContactsActivity;
 import com.android.contacts.R;
-import com.android.contacts.model.ExchangeAccountType;
-import com.android.contacts.model.GoogleAccountType;
+import com.android.contacts.model.AccountType;
+import com.android.contacts.model.EntityDelta;
+import com.android.contacts.model.EntityDeltaList;
+import com.android.contacts.model.EntityModifier;
+import com.android.contacts.util.ContactPhotoUtils;
+import com.android.contacts.ContactLoader;
+import com.android.contacts.ContactSaveService;
+import com.android.contacts.ContactsUtils;
 
-import android.content.ContentProviderOperation;
 import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
 import android.content.Intent;
-import android.content.OperationApplicationException;
+import android.content.Loader;
+import android.content.Loader.OnLoadCompleteListener;
 import android.database.Cursor;
 import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
 import android.net.Uri;
 import android.os.Bundle;
-import android.os.RemoteException;
-import android.provider.ContactsContract;
 import android.provider.ContactsContract.CommonDataKinds.Photo;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.DisplayPhoto;
-import android.provider.ContactsContract.RawContacts;
-import android.widget.Toast;
+import android.provider.MediaStore;
+import android.util.Log;
 
-import java.io.ByteArrayOutputStream;
-import java.util.ArrayList;
+import java.io.File;
 
 /**
  * Provides an external interface for other applications to attach images
@@ -49,25 +51,38 @@
  * size and give the user a chance to use the face detector.
  */
 public class AttachPhotoActivity extends ContactsActivity {
+    private static final String TAG = AttachPhotoActivity.class.getSimpleName();
+
     private static final int REQUEST_PICK_CONTACT = 1;
     private static final int REQUEST_CROP_PHOTO = 2;
 
-    private static final String RAW_CONTACT_URIS_KEY = "raw_contact_uris";
+    private static final String KEY_CONTACT_URI = "contact_uri";
+    private static final String KEY_TEMP_PHOTO_URI = "temp_photo_uri";
 
-    private Long[] mRawContactIds;
+    private File mTempPhotoFile;
+    private Uri mTempPhotoUri;
 
     private ContentResolver mContentResolver;
 
-    // Height/width (in pixels) to request for the photo - queried from the provider.
+    // Height and width (in pixels) to request for the photo - queried from the provider.
     private static int mPhotoDim;
 
+    private Uri mContactUri;
+
     @Override
     public void onCreate(Bundle icicle) {
         super.onCreate(icicle);
 
         if (icicle != null) {
-            mRawContactIds = toClassArray(icicle.getLongArray(RAW_CONTACT_URIS_KEY));
+            final String uri = icicle.getString(KEY_CONTACT_URI);
+            mContactUri = (uri == null) ? null : Uri.parse(uri);
+
+            mTempPhotoUri = Uri.parse(icicle.getString(KEY_TEMP_PHOTO_URI));
+            mTempPhotoFile = new File(mTempPhotoUri.getPath());
         } else {
+            mTempPhotoFile = ContactPhotoUtils.generateTempPhotoFile();
+            mTempPhotoUri = Uri.fromFile(mTempPhotoFile);
+
             Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
             intent.setType(Contacts.CONTENT_ITEM_TYPE);
             startActivityForResult(intent, REQUEST_PICK_CONTACT);
@@ -89,32 +104,8 @@
     @Override
     protected void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
-
-        if (mRawContactIds != null && mRawContactIds.length != 0) {
-            outState.putLongArray(RAW_CONTACT_URIS_KEY, toPrimativeArray(mRawContactIds));
-        }
-    }
-
-    private static long[] toPrimativeArray(Long[] in) {
-        if (in == null) {
-            return null;
-        }
-        long[] out = new long[in.length];
-        for (int i = 0; i < in.length; i++) {
-            out[i] = in[i];
-        }
-        return out;
-    }
-
-    private static Long[] toClassArray(long[] in) {
-        if (in == null) {
-            return null;
-        }
-        Long[] out = new Long[in.length];
-        for (int i = 0; i < in.length; i++) {
-            out[i] = in[i];
-        }
-        return out;
+        if (mContactUri != null) outState.putString(KEY_CONTACT_URI, mContactUri.toString());
+        outState.putString(KEY_TEMP_PHOTO_URI, mTempPhotoUri.toString());
     }
 
     @Override
@@ -137,147 +128,94 @@
             intent.putExtra("aspectY", 1);
             intent.putExtra("outputX", mPhotoDim);
             intent.putExtra("outputY", mPhotoDim);
-            intent.putExtra("return-data", true);
+            intent.putExtra(MediaStore.EXTRA_OUTPUT, mTempPhotoUri);
+
             startActivityForResult(intent, REQUEST_CROP_PHOTO);
 
-            // while they're cropping, convert the contact into a raw_contact
-            final long contactId = ContentUris.parseId(result.getData());
-            final ArrayList<Long> rawContactIdsList = queryForAllRawContactIds(
-                    mContentResolver, contactId);
-            mRawContactIds = new Long[rawContactIdsList.size()];
-            mRawContactIds = rawContactIdsList.toArray(mRawContactIds);
+            mContactUri = result.getData();
 
-            if (mRawContactIds == null || rawContactIdsList.isEmpty()) {
-                Toast.makeText(this, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
-            }
         } else if (requestCode == REQUEST_CROP_PHOTO) {
-            final Bundle extras = result.getExtras();
-            if (extras != null && mRawContactIds != null) {
-                Bitmap photo = extras.getParcelable("data");
-                if (photo != null) {
-                    ByteArrayOutputStream stream = new ByteArrayOutputStream();
-                    photo.compress(Bitmap.CompressFormat.PNG, 100, stream);
-
-                    final ContentValues imageValues = new ContentValues();
-                    imageValues.put(Photo.PHOTO, stream.toByteArray());
-                    imageValues.put(RawContacts.Data.IS_SUPER_PRIMARY, 1);
-
-                    // attach the photo to every raw contact
-                    for (Long rawContactId : mRawContactIds) {
-
-                        // exchange and google only allow one image, so do an update rather than insert
-                        boolean shouldUpdate = false;
-
-                        final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
-                                rawContactId);
-                        final Uri rawContactDataUri = Uri.withAppendedPath(rawContactUri,
-                                RawContacts.Data.CONTENT_DIRECTORY);
-                        insertPhoto(imageValues, rawContactDataUri, true);
-                    }
+            loadContact(mContactUri, new ContactLoader.Listener() {
+                @Override
+                public void onContactLoaded(ContactLoader.Result contact) {
+                    saveContact(contact);
                 }
-            }
-            finish();
+            });
         }
     }
 
-    // TODO: move to background
-    public static ArrayList<Long> queryForAllRawContactIds(ContentResolver cr, long contactId) {
-        Cursor rawContactIdCursor = null;
-        ArrayList<Long> rawContactIds = new ArrayList<Long>();
-        try {
-            rawContactIdCursor = cr.query(RawContacts.CONTENT_URI,
-                    new String[] {RawContacts._ID},
-                    RawContacts.CONTACT_ID + "=" + contactId, null, null);
-            if (rawContactIdCursor != null) {
-                while (rawContactIdCursor.moveToNext()) {
-                    rawContactIds.add(rawContactIdCursor.getLong(0));
+    // TODO: consider moving this to ContactLoader, especially if we keep adding similar
+    // code elsewhere (ViewNotificationService is another case).  The only concern is that,
+    // although this is convenient, it isn't quite as robust as using LoaderManager... for
+    // instance, the loader doesn't persist across Activity restarts.
+    private void loadContact(Uri contactUri, final ContactLoader.Listener listener) {
+        final ContactLoader loader = new ContactLoader(this, contactUri);
+        loader.registerListener(0, new OnLoadCompleteListener<ContactLoader.Result>() {
+            @Override
+            public void onLoadComplete(
+                    Loader<ContactLoader.Result> loader, ContactLoader.Result contact) {
+                try {
+                    loader.reset();
                 }
+                catch (RuntimeException e) {
+                    Log.e(TAG, "Error resetting loader", e);
+                }
+                listener.onContactLoaded(contact);
             }
-        } finally {
-            if (rawContactIdCursor != null) {
-                rawContactIdCursor.close();
-            }
-        }
-        return rawContactIds;
+        });
+        loader.startLoading();
     }
 
     /**
-     * Inserts a photo on the raw contact.
-     * @param values the photo values
-     * @param assertAccount if true, will check to verify that no photos exist for Google,
-     *     Exchange and unsynced phone account types. These account types only take one picture,
-     *     so if one exists, the account will be updated with the new photo.
+     * If prerequisites have been met, attach the photo to a raw-contact and save.
+     * The prerequisites are:
+     * - photo has been cropped
+     * - contact has been loaded
      */
-    private void insertPhoto(ContentValues values, Uri rawContactDataUri,
-            boolean assertAccount) {
+    private void saveContact(ContactLoader.Result contact) {
 
-        ArrayList<ContentProviderOperation> operations =
-            new ArrayList<ContentProviderOperation>();
-
-        if (assertAccount) {
-            // Make sure no pictures exist for Google, Exchange and unsynced phone accounts.
-            operations.add(ContentProviderOperation.newAssertQuery(rawContactDataUri)
-                    .withSelection(Photo.MIMETYPE + "=? AND "
-                            + RawContacts.DATA_SET + " IS NULL AND ("
-                            + RawContacts.ACCOUNT_TYPE + " IN (?,?) OR "
-                            + RawContacts.ACCOUNT_TYPE + " IS NULL)",
-                            new String[] {Photo.CONTENT_ITEM_TYPE, GoogleAccountType.ACCOUNT_TYPE,
-                            ExchangeAccountType.ACCOUNT_TYPE})
-                            .withExpectedCount(0).build());
+        // Obtain the raw-contact that we will save to.
+        EntityDeltaList deltaList = contact.createEntityDeltaList();
+        EntityDelta raw = deltaList.getFirstWritableRawContact(this);
+        if (raw == null) {
+            Log.w(TAG, "no writable raw-contact found");
+            return;
         }
 
-        // insert the photo
-        values.put(Photo.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
-        operations.add(ContentProviderOperation.newInsert(rawContactDataUri)
-                .withValues(values).build());
-
-        try {
-            mContentResolver.applyBatch(ContactsContract.AUTHORITY, operations);
-        } catch (RemoteException e) {
-            throw new IllegalStateException("Problem querying raw_contacts/data", e);
-        } catch (OperationApplicationException e) {
-            // the account doesn't allow multiple photos, so update
-            if (assertAccount) {
-                updatePhoto(values, rawContactDataUri, false);
-            } else {
-                throw new IllegalStateException("Problem inserting photo into raw_contacts/data", e);
-            }
+        // Create a scaled, compressed bitmap to add to the entity-delta list.
+        final int size = ContactsUtils.getThumbnailSize(this);
+        final Bitmap bitmap = BitmapFactory.decodeFile(mTempPhotoFile.getAbsolutePath());
+        final Bitmap scaled = Bitmap.createScaledBitmap(bitmap, size, size, false);
+        final byte[] compressed = ContactPhotoUtils.compressBitmap(scaled);
+        if (compressed == null) {
+            Log.w(TAG, "could not create scaled and compressed Bitmap");
+            return;
         }
-    }
 
-    /**
-     * Tries to update the photo on the raw_contact.  If no photo exists, and allowInsert == true,
-     * then will try to {@link #updatePhoto(ContentValues, boolean)}
-     */
-    private void updatePhoto(ContentValues values, Uri rawContactDataUri,
-            boolean allowInsert) {
-        ArrayList<ContentProviderOperation> operations =
-            new ArrayList<ContentProviderOperation>();
-
-        values.remove(Photo.MIMETYPE);
-
-        // check that a photo exists
-        operations.add(ContentProviderOperation.newAssertQuery(rawContactDataUri)
-                .withSelection(Photo.MIMETYPE + "=?", new String[] {
-                    Photo.CONTENT_ITEM_TYPE
-                }).withExpectedCount(1).build());
-
-        // update that photo
-        operations.add(ContentProviderOperation.newUpdate(rawContactDataUri)
-                .withSelection(Photo.MIMETYPE + "=?", new String[] {Photo.CONTENT_ITEM_TYPE})
-                .withValues(values).build());
-
-        try {
-            mContentResolver.applyBatch(ContactsContract.AUTHORITY, operations);
-        } catch (RemoteException e) {
-            throw new IllegalStateException("Problem querying raw_contacts/data", e);
-        } catch (OperationApplicationException e) {
-            if (allowInsert) {
-                // they deleted the photo between insert and update, so insert one
-                insertPhoto(values, rawContactDataUri, false);
-            } else {
-                throw new IllegalStateException("Problem inserting photo raw_contacts/data", e);
-            }
+        // Add compressed bitmap to entity-delta... this allows us to save to
+        // a new contact; otherwise the entity-delta-list would be empty, and
+        // the ContactSaveService would not create the new contact, and the
+        // full-res photo would fail to be saved to the non-existent contact.
+        AccountType account = raw.getRawContactAccountType(this);
+        EntityDelta.ValuesDelta values =
+                EntityModifier.ensureKindExists(raw, account, Photo.CONTENT_ITEM_TYPE);
+        if (values == null) {
+            Log.w(TAG, "cannot attach photo to this account type");
+            return;
         }
+        values.put(Photo.PHOTO, compressed);
+
+        // Finally, invoke the ContactSaveService.
+        Log.v(TAG, "all prerequisites met, about to save photo to contact");
+        Intent intent = ContactSaveService.createSaveContactIntent(
+                this,
+                deltaList,
+                "", 0,
+                contact.isUserProfile(),
+                null, null,
+                raw.getRawContactId(),
+                mTempPhotoFile.getAbsolutePath());
+        startService(intent);
+        finish();
     }
 }
diff --git a/src/com/android/contacts/activities/ConfirmAddDetailActivity.java b/src/com/android/contacts/activities/ConfirmAddDetailActivity.java
index 98abfbc..aa3be87 100644
--- a/src/com/android/contacts/activities/ConfirmAddDetailActivity.java
+++ b/src/com/android/contacts/activities/ConfirmAddDetailActivity.java
@@ -560,19 +560,10 @@
     }
 
     public void findEditableRawContact() {
-        if (mEntityDeltaList == null) {
-            return;
-        }
-        for (EntityDelta state : mEntityDeltaList) {
-            final String accountType = state.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
-            final String dataSet = state.getValues().getAsString(RawContacts.DATA_SET);
-            final AccountType type = mAccountTypeManager.getAccountType(accountType, dataSet);
-
-            if (type.areContactsWritable()) {
-                mEditableAccountType = type;
-                mState = state;
-                return;
-            }
+        if (mEntityDeltaList == null) return;
+        mState = mEntityDeltaList.getFirstWritableRawContact(this);
+        if (mState != null) {
+            mEditableAccountType = mState.getRawContactAccountType(this);
         }
     }
 
diff --git a/src/com/android/contacts/activities/PhotoSelectionActivity.java b/src/com/android/contacts/activities/PhotoSelectionActivity.java
index d76af25..d443782 100644
--- a/src/com/android/contacts/activities/PhotoSelectionActivity.java
+++ b/src/com/android/contacts/activities/PhotoSelectionActivity.java
@@ -449,45 +449,36 @@
     }
 
     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 PhotoActionListener mListener;
+
+        private PhotoHandler(
+                Context context, View photoView, int photoMode, EntityDeltaList state) {
+            super(context, photoView, photoMode, PhotoSelectionActivity.this.mIsDirectoryContact,
+                    state);
+            mListener = new PhotoListener();
+        }
+
+        @Override
+        public PhotoActionListener getListener() {
+            return mListener;
+        }
+
+        @Override
+        public void startPhotoActivity(Intent intent, int requestCode, File photoFile) {
+            mSubActivityInProgress = true;
+            mCurrentPhotoFile = photoFile;
+            PhotoSelectionActivity.this.startActivityForResult(intent, requestCode);
         }
 
         private final class PhotoListener extends PhotoActionListener {
-            @SuppressWarnings("hiding")
-            private final boolean mIsProfile;
-            private final Context mContext;
-
-            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,
-                    File photoFile) {
-                mSubActivityInProgress = true;
-                mCurrentPhotoFile = photoFile;
-                startActivityForResult(intent, requestCode);
-            }
 
             @Override
             public void onPhotoSelected(Bitmap bitmap) {
                 EntityDeltaList delta = getDeltaForAttachingPhotoToContact();
                 long rawContactId = getWritableEntityId();
                 String filePath = mCurrentPhotoFile.getAbsolutePath();
-                Intent intent = ContactSaveService.createSaveContactIntent(mContext, delta,
-                        "", 0, mIsProfile, PhotoSelectionActivity.class,
-                        ContactEditorActivity.ACTION_SAVE_COMPLETED, rawContactId, filePath);
+                Intent intent = ContactSaveService.createSaveContactIntent(
+                        mContext, delta, "", 0, mIsProfile, null, null, rawContactId, filePath);
                 startService(intent);
                 finish();
             }
diff --git a/src/com/android/contacts/detail/ContactDetailFragment.java b/src/com/android/contacts/detail/ContactDetailFragment.java
index 2dc7bc4..34250d5 100644
--- a/src/com/android/contacts/detail/ContactDetailFragment.java
+++ b/src/com/android/contacts/detail/ContactDetailFragment.java
@@ -2016,8 +2016,7 @@
             if (defaultGroupId == -1) return;
 
             // add the group membership to the current state
-            final EntityDeltaList contactDeltaList = EntityDeltaList.fromIterator(
-                    mContactData.getEntities().iterator());
+            final EntityDeltaList contactDeltaList = mContactData.createEntityDeltaList();
             final EntityDelta rawContactEntityDelta = contactDeltaList.get(0);
 
             final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
diff --git a/src/com/android/contacts/detail/ContactDetailPhotoSetter.java b/src/com/android/contacts/detail/ContactDetailPhotoSetter.java
index 13f5970..b674dc0 100644
--- a/src/com/android/contacts/detail/ContactDetailPhotoSetter.java
+++ b/src/com/android/contacts/detail/ContactDetailPhotoSetter.java
@@ -63,8 +63,7 @@
         @Override
         public void onClick(View v) {
             // Assemble the intent.
-            EntityDeltaList delta = EntityDeltaList.fromIterator(
-                    mContactData.getEntities().iterator());
+            EntityDeltaList delta = mContactData.createEntityDeltaList();
 
             // Find location and bounds of target view, adjusting based on the
             // assumed local density.
diff --git a/src/com/android/contacts/detail/PhotoSelectionHandler.java b/src/com/android/contacts/detail/PhotoSelectionHandler.java
index 1423c65..b336d90 100644
--- a/src/com/android/contacts/detail/PhotoSelectionHandler.java
+++ b/src/com/android/contacts/detail/PhotoSelectionHandler.java
@@ -19,15 +19,14 @@
 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 com.android.contacts.util.ContactPhotoUtils;
 
 import android.app.Activity;
 import android.content.ActivityNotFoundException;
-import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.database.Cursor;
@@ -35,10 +34,8 @@
 import android.graphics.BitmapFactory;
 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;
@@ -46,30 +43,20 @@
 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 {
+public abstract 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;
+    protected final Context mContext;
     private final View mPhotoView;
     private final int mPhotoMode;
     private final int mPhotoPickSize;
@@ -87,6 +74,8 @@
         mIsDirectoryContact = isDirectoryContact;
         mState = state;
         mPhotoPickSize = getPhotoPickSize();
+
+        // NOTE: subclasses should call setListener()
     }
 
     public void destroy() {
@@ -95,13 +84,7 @@
         }
     }
 
-    public PhotoActionListener getListener() {
-        return mListener;
-    }
-
-    public void setListener(PhotoActionListener listener) {
-        mListener = listener;
-    }
+    public abstract PhotoActionListener getListener();
 
     @Override
     public void onClick(View v) {
@@ -109,10 +92,11 @@
             if (getWritableEntityIndex() != -1) {
                 mPopup = PhotoActionPopup.createPopupMenu(
                         mContext, mPhotoView, mListener, mPhotoMode);
+                final PhotoActionListener listener = mListener; // a bit more bulletproof
                 mPopup.setOnDismissListener(new OnDismissListener() {
                     @Override
                     public void onDismiss() {
-                        mListener.onPhotoSelectionDismissed();
+                        listener.onPhotoSelectionDismissed();
                     }
                 });
                 mPopup.show();
@@ -152,25 +136,8 @@
      */
     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;
+        if (mIsDirectoryContact) return -1;
+        return mState.indexOfFirstWritableRawContact(mContext);
     }
 
     /**
@@ -187,7 +154,6 @@
      * 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.
@@ -208,6 +174,9 @@
         return null;
     }
 
+    /** Used by subclasses to delegate to their enclosing Activity or Fragment. */
+    protected abstract void startPhotoActivity(Intent intent, int requestCode, File photoFile);
+
     /**
      * Sends a newly acquired photo to Gallery for cropping
      */
@@ -222,22 +191,33 @@
 
             // Launch gallery to crop the photo
             final Intent intent = getCropImageIntent(f);
-            mListener.startPickFromGalleryActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA, f);
+            startPhotoActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA, f);
         } 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";
+    /**
+     * Should initiate an activity to take a photo using the camera.
+     * @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()}.
+     */
+    private void startTakePhotoActivity(File photoFile) {
+        final Intent intent = getTakePhotoIntent(photoFile);
+        startPhotoActivity(intent, REQUEST_CODE_CAMERA_WITH_DATA, photoFile);
     }
 
-    private File getPhotoFile() {
-        PHOTO_DIR.mkdirs();
-        return new File(PHOTO_DIR, getPhotoFileName());
+    /**
+     * Should initiate an activity pick a photo from the gallery.
+     * @param photoFile The temporary file that the cropped image is written to before being
+     *     stored by the content-provider.
+     *     {@link PhotoSelectionHandler#handlePhotoActivityResult(int, int, Intent)}.
+     */
+    private void startPickFromGalleryActivity(File photoFile) {
+        final Intent intent = getPhotoPickIntent(photoFile);
+        startPhotoActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA, photoFile);
     }
 
     private int getPhotoPickSize() {
@@ -308,9 +288,7 @@
         public void onTakePhotoChosen() {
             try {
                 // Launch camera to take photo for selected contact
-                File f = getPhotoFile();
-                final Intent intent = getTakePhotoIntent(f);
-                startTakePhotoActivity(intent, REQUEST_CODE_CAMERA_WITH_DATA, f);
+                startTakePhotoActivity(ContactPhotoUtils.generateTempPhotoFile());
             } catch (ActivityNotFoundException e) {
                 Toast.makeText(mContext, R.string.photoPickerNotFoundText,
                         Toast.LENGTH_LONG).show();
@@ -321,9 +299,7 @@
         public void onPickFromGalleryChosen() {
             try {
                 // Launch picker to choose photo for selected contact
-                File f = getPhotoFile();
-                final Intent intent = getPhotoPickIntent(f);
-                startPickFromGalleryActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA, f);
+                startPickFromGalleryActivity(ContactPhotoUtils.generateTempPhotoFile());
             } catch (ActivityNotFoundException e) {
                 Toast.makeText(mContext, R.string.photoPickerNotFoundText,
                         Toast.LENGTH_LONG).show();
@@ -331,28 +307,6 @@
         }
 
         /**
-         * 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
-         * @param photoFile The temporary file that the cropped image is written to before being
-         *     stored by the content-provider.
-         *     {@link PhotoSelectionHandler#handlePhotoActivityResult(int, int, Intent)}.
-         */
-        public abstract void startPickFromGalleryActivity(Intent intent, int requestCode,
-                File photoFile);
-
-        /**
          * Called when the user has completed selection of a photo.
          * @param bitmap The selected and cropped photo.
          */
diff --git a/src/com/android/contacts/editor/ContactEditorFragment.java b/src/com/android/contacts/editor/ContactEditorFragment.java
index b9f1afc..d93fb5c 100644
--- a/src/com/android/contacts/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorFragment.java
@@ -462,15 +462,15 @@
         mListener.onCustomEditContactActivityRequested(account, uri, null, false);
     }
 
-    private void bindEditorsForExistingContact(ContactLoader.Result data) {
+    private void bindEditorsForExistingContact(ContactLoader.Result contact) {
         setEnabled(true);
 
-        mState = EntityDeltaList.fromIterator(data.getEntities().iterator());
+        mState = contact.createEntityDeltaList();
         setIntentExtras(mIntentExtras);
         mIntentExtras = null;
 
         // For user profile, change the contacts query URI
-        mIsUserProfile = data.isUserProfile();
+        mIsUserProfile = contact.isUserProfile();
         boolean localProfileExists = false;
 
         if (mIsUserProfile) {
@@ -1714,21 +1714,32 @@
     private final class PhotoHandler extends PhotoSelectionHandler {
 
         final long mRawContactId;
+        private final BaseRawContactEditorView mEditor;
+        private PhotoActionListener mListener;
 
         public PhotoHandler(Context context, BaseRawContactEditorView editor, int photoMode,
                 EntityDeltaList state) {
             super(context, editor.getPhotoEditor(), photoMode, false, state);
-            setListener(new PhotoEditorListener(editor));
+            mEditor = editor;
             mRawContactId = editor.getRawContactId();
+            mListener = new PhotoEditorListener();
+        }
+
+        @Override
+        public PhotoActionListener getListener() {
+            return mListener;
+        }
+
+        @Override
+        public void startPhotoActivity(Intent intent, int requestCode, File photoFile) {
+            mRawContactIdRequestingPhoto = mEditor.getRawContactId();
+            mStatus = Status.SUB_ACTIVITY;
+            mCurrentPhotoFile = photoFile;
+            ContactEditorFragment.this.startActivityForResult(intent, requestCode);
         }
 
         private final class PhotoEditorListener extends PhotoSelectionHandler.PhotoActionListener
                 implements EditorListener {
-            private final BaseRawContactEditorView mEditor;
-
-            private PhotoEditorListener(BaseRawContactEditorView editor) {
-                mEditor = editor;
-            }
 
             @Override
             public void onRequest(int request) {
@@ -1776,23 +1787,6 @@
             }
 
             @Override
-            public void startTakePhotoActivity(Intent intent, int requestCode, File photoFile) {
-                mRawContactIdRequestingPhoto = mEditor.getRawContactId();
-                mStatus = Status.SUB_ACTIVITY;
-                mCurrentPhotoFile = photoFile;
-                startActivityForResult(intent, requestCode);
-            }
-
-            @Override
-            public void startPickFromGalleryActivity(Intent intent, int requestCode,
-                    File photoFile) {
-                mRawContactIdRequestingPhoto = mEditor.getRawContactId();
-                mStatus = Status.SUB_ACTIVITY;
-                mCurrentPhotoFile = photoFile;
-                startActivityForResult(intent, requestCode);
-            }
-
-            @Override
             public void onPhotoSelected(Bitmap bitmap) {
                 setPhoto(mRawContactIdRequestingPhoto, bitmap, mCurrentPhotoFile);
                 mRawContactIdRequestingPhoto = -1;
diff --git a/src/com/android/contacts/editor/PhotoEditorView.java b/src/com/android/contacts/editor/PhotoEditorView.java
index 8a0dd0e..0cbe97e 100644
--- a/src/com/android/contacts/editor/PhotoEditorView.java
+++ b/src/com/android/contacts/editor/PhotoEditorView.java
@@ -20,6 +20,7 @@
 import com.android.contacts.model.DataKind;
 import com.android.contacts.model.EntityDelta;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.util.ContactPhotoUtils;
 import com.android.contacts.ContactsUtils;
 
 import android.content.Context;
@@ -27,19 +28,14 @@
 import android.graphics.BitmapFactory;
 import android.provider.ContactsContract.CommonDataKinds.Photo;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.view.View;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-
 /**
  * Simple editor for {@link Photo}.
  */
 public class PhotoEditorView extends FrameLayout implements Editor {
-    private static final String TAG = "PhotoEditorView";
 
     private ImageView mPhotoImageView;
     private View mFrameView;
@@ -127,23 +123,7 @@
         return mHasSetPhoto;
     }
 
-    /**
-     * Creates a byte[] containing the PNG-compressed bitmap, or null if
-     * something goes wrong.
-     */
-    private static byte[] compressBitmap(Bitmap bitmap) {
-        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();
-            return out.toByteArray();
-        } catch (IOException e) {
-            Log.w(TAG, "Unable to serialize photo: " + e.toString());
-            return null;
-        }
-    }
+
 
     /**
      * Assign the given {@link Bitmap} as the new value, updating UI and
@@ -172,7 +152,8 @@
         // 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 int size = ContactsUtils.getThumbnailSize(getContext());
-        byte[] compressed = compressBitmap(Bitmap.createScaledBitmap(photo, size, size, false));
+        final Bitmap scaled = Bitmap.createScaledBitmap(photo, size, size, false);
+        final byte[] compressed = ContactPhotoUtils.compressBitmap(scaled);
         if (compressed != null) mEntry.put(Photo.PHOTO, compressed);
     }
 
diff --git a/src/com/android/contacts/model/EntityDelta.java b/src/com/android/contacts/model/EntityDelta.java
index 2cbfa26..2620fb0 100644
--- a/src/com/android/contacts/model/EntityDelta.java
+++ b/src/com/android/contacts/model/EntityDelta.java
@@ -24,6 +24,7 @@
 import android.content.ContentProviderOperation;
 import android.content.ContentProviderOperation.Builder;
 import android.content.ContentValues;
+import android.content.Context;
 import android.content.Entity;
 import android.content.Entity.NamedContentValues;
 import android.net.Uri;
@@ -210,6 +211,20 @@
     }
 
     /**
+     * Return the AccountType that this raw-contact belongs to.
+     */
+    public AccountType getRawContactAccountType(Context context) {
+        ContentValues entityValues = getValues().getCompleteValues();
+        String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
+        String dataSet = entityValues.getAsString(RawContacts.DATA_SET);
+        return AccountTypeManager.getInstance(context).getAccountType(type, dataSet);
+    }
+
+    public Long getRawContactId() {
+        return getValues().getAsLong(RawContacts._ID);
+    }
+
+    /**
      * Return the list of child {@link ValuesDelta} from our optimized map,
      * creating the list if requested.
      */
diff --git a/src/com/android/contacts/model/EntityDeltaList.java b/src/com/android/contacts/model/EntityDeltaList.java
index 5a9355b..478c879 100644
--- a/src/com/android/contacts/model/EntityDeltaList.java
+++ b/src/com/android/contacts/model/EntityDeltaList.java
@@ -18,6 +18,7 @@
 
 import android.content.ContentProviderOperation;
 import android.content.ContentResolver;
+import android.content.Context;
 import android.content.Entity;
 import android.content.EntityIterator;
 import android.content.ContentProviderOperation.Builder;
@@ -27,7 +28,6 @@
 import android.provider.ContactsContract.AggregationExceptions;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.RawContacts;
-import android.provider.ContactsContract.RawContactsEntity;
 
 import com.google.android.collect.Lists;
 
@@ -35,7 +35,6 @@
 
 import java.util.ArrayList;
 import java.util.Iterator;
-import java.util.List;
 
 /**
  * Container for multiple {@link EntityDelta} objects, usually when editing
@@ -290,6 +289,9 @@
         return null;
     }
 
+    /**
+     * Find the raw-contact (an {@link EntityDelta}) with the specified ID.
+     */
     public EntityDelta getByRawContactId(Long rawContactId) {
         final int index = this.indexOfRawContactId(rawContactId);
         return (index == -1) ? null : this.get(index);
@@ -310,6 +312,23 @@
         return -1;
     }
 
+    /** Return the index of the first EntityDelta corresponding to a writable raw-contact, or -1. */
+    public int indexOfFirstWritableRawContact(Context context) {
+        // Find the first writable entity.
+        int entityIndex = 0;
+        for (EntityDelta delta : this) {
+            if (delta.getRawContactAccountType(context).areContactsWritable()) return entityIndex;
+            entityIndex++;
+        }
+        return -1;
+    }
+
+    /**  Return the first EntityDelta corresponding to a writable raw-contact, or null. */
+    public EntityDelta getFirstWritableRawContact(Context context) {
+        final int index = indexOfFirstWritableRawContact(context);
+        return (index == -1) ? null : get(index);
+    }
+
     public ValuesDelta getSuperPrimaryEntry(final String mimeType) {
         ValuesDelta primary = null;
         ValuesDelta randomEntry = null;
@@ -354,12 +373,14 @@
     }
 
     /** {@inheritDoc} */
+    @Override
     public int describeContents() {
         // Nothing special about this parcel
         return 0;
     }
 
     /** {@inheritDoc} */
+    @Override
     public void writeToParcel(Parcel dest, int flags) {
         final int size = this.size();
         dest.writeInt(size);
@@ -383,12 +404,14 @@
 
     public static final Parcelable.Creator<EntityDeltaList> CREATOR =
             new Parcelable.Creator<EntityDeltaList>() {
+        @Override
         public EntityDeltaList createFromParcel(Parcel in) {
             final EntityDeltaList state = new EntityDeltaList();
             state.readFromParcel(in);
             return state;
         }
 
+        @Override
         public EntityDeltaList[] newArray(int size) {
             return new EntityDeltaList[size];
         }
diff --git a/src/com/android/contacts/util/ContactPhotoUtils.java b/src/com/android/contacts/util/ContactPhotoUtils.java
new file mode 100644
index 0000000..f214e9f
--- /dev/null
+++ b/src/com/android/contacts/util/ContactPhotoUtils.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2012 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.util;
+
+import android.graphics.Bitmap;
+import android.os.Environment;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * Utilities related to loading/saving contact photos.
+ *
+ */
+public class ContactPhotoUtils {
+    private static final String TAG = "ContactPhotoUtils";
+
+    private static final String PHOTO_DATE_FORMAT = "'IMG'_yyyyMMdd_HHmmss";
+
+    // TODO: /DCIM/Camera isn't the ideal place to stash cropped contact photos.
+    //       Where is the right place?
+    private static final File PHOTO_DIR = new File(
+            Environment.getExternalStorageDirectory() + "/DCIM/Camera");
+
+    /**
+     * Generate a new, unique file to be used as an out-of-band communication
+     * channel, since hi-res Bitmaps are too big to serialize into a Bundle.
+     * This file will be passed to other activities (such as the gallery/camera/cropper/etc.),
+     * and read by us once they are finished writing it.
+     */
+    public static File generateTempPhotoFile() {
+        PHOTO_DIR.mkdirs();
+        return new File(PHOTO_DIR, generateTempPhotoFileName());
+    }
+
+    private static String generateTempPhotoFileName() {
+        Date date = new Date(System.currentTimeMillis());
+        SimpleDateFormat dateFormat = new SimpleDateFormat(PHOTO_DATE_FORMAT);
+        return dateFormat.format(date) + ".jpg";
+    }
+
+    /**
+     * Creates a byte[] containing the PNG-compressed bitmap, or null if
+     * something goes wrong.
+     */
+    public static byte[] compressBitmap(Bitmap bitmap) {
+        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();
+            return out.toByteArray();
+        } catch (IOException e) {
+            Log.w(TAG, "Unable to serialize photo: " + e.toString());
+            return null;
+        }
+    }
+}
+
+