am e286c04b: Merge "Support for high-resolution contact photos."

* commit 'e286c04b300feb9e4bdd13281a764103c1548323':
  Support for high-resolution contact photos.
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index be84cc4..5dd1942 100644
--- a/src/com/android/contacts/ContactSaveService.java
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -35,11 +35,13 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.OperationApplicationException;
+import android.content.res.AssetFileDescriptor;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Parcelable;
+import android.os.Bundle;
 import android.os.RemoteException;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.AggregationExceptions;
@@ -53,10 +55,16 @@
 import android.util.Log;
 import android.widget.Toast;
 
+import java.lang.Long;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.Iterator;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
 
 /**
  * A service responsible for saving changes to the content provider.
@@ -80,6 +88,7 @@
     public static final String EXTRA_SAVE_MODE = "saveMode";
     public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
     public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
+    public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
 
     public static final String ACTION_CREATE_GROUP = "createGroup";
     public static final String ACTION_RENAME_GROUP = "renameGroup";
@@ -269,15 +278,38 @@
     /**
      * Creates an intent that can be sent to this service to create a new raw contact
      * using data presented as a set of ContentValues.
+     * This variant is more convenient to use when there is only one photo that can
+     * possibly be updated, as in the Contact Details screen.
+     * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
+     * @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) {
+            String callbackAction, long rawContactId, String updatedPhotoPath) {
+        Bundle bundle = new Bundle();
+        bundle.putString(String.valueOf(rawContactId), updatedPhotoPath);
+        return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
+                callbackActivity, callbackAction, bundle);
+    }
+
+    /**
+     * Creates an intent that can be sent to this service to create a new raw contact
+     * using data presented as a set of ContentValues.
+     * This variant is used when multiple contacts' photos may be updated, as in the
+     * Contact Editor.
+     * @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) {
         Intent serviceIntent = new Intent(
                 context, ContactSaveService.class);
         serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
         serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
         serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
+        if (updatedPhotos != null) {
+            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
@@ -293,6 +325,7 @@
         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);
 
         // Trim any empty fields, and RawContacts, before persisting
         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
@@ -301,6 +334,7 @@
         Uri lookupUri = null;
 
         final ContentResolver resolver = getContentResolver();
+        boolean succeeded = false;
 
         // Attempt to persist changes
         int tries = 0;
@@ -346,10 +380,9 @@
                     lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
                 }
                 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
-                // 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);
+
+                // We can change this back to false later, if we fail to save the contact photo.
+                succeeded = true;
                 break;
 
             } catch (RemoteException e) {
@@ -395,11 +428,69 @@
             }
         }
 
+        // Now save any updated photos.  We do this at the end to ensure that
+        // the ContactProvider already knows about newly-created contacts.
+        if (updatedPhotos != null) {
+            for (String key : updatedPhotos.keySet()) {
+                String photoFilePath = updatedPhotos.getString(key);
+                long rawContactId = Long.parseLong(key);
+                File photoFile = new File(photoFilePath);
+                if (!saveUpdatedPhoto(rawContactId, photoFile)) succeeded = false;
+            }
+        }
+
+        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);
     }
 
+    /**
+     * Save updated photo for the specified raw-contact.
+     * @return true for success, false for failure
+     */
+    private boolean saveUpdatedPhoto(long rawContactId, File photoFile) {
+        Uri outputUri = Uri.withAppendedPath(
+                ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
+                RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
+
+        FileOutputStream outputStream = null;
+        FileInputStream inputStream = null;
+        byte[] buffer = new byte[16 * 1024];
+        int length;
+        int totalLength = 0;
+        try {
+            AssetFileDescriptor fd = getContentResolver().openAssetFileDescriptor(outputUri, "rw");
+            outputStream = fd.createOutputStream();
+            inputStream = new FileInputStream(photoFile);
+            while ((length = inputStream.read(buffer)) > 0) {
+                outputStream.write(buffer, 0, length);
+                totalLength += length;
+            }
+            return true; // yay!
+        } catch(IOException e) {
+            Log.e(TAG, "Failed to write photo: " + photoFile.toString() + " because: " + e);
+        } finally {
+            Log.v(TAG, "Wrote " + totalLength + " bytes for photo " + photoFile.toString());
+            try {
+                inputStream.close();
+            } catch(IOException e) {
+                Log.e(TAG, "Failed to close photo input stream");
+            }
+            try {
+                outputStream.close();
+            } catch(IOException e) {
+                Log.e(TAG, "Failed to close photo output stream");
+            }
+        }
+        return false; // failed
+    }
+
     private long getRawContactId(EntityDeltaList state,
             final ArrayList<ContentProviderOperation> diff,
             final ContentProviderResult[] results) {
diff --git a/src/com/android/contacts/activities/PhotoSelectionActivity.java b/src/com/android/contacts/activities/PhotoSelectionActivity.java
index 9ac0fa8..02843e9 100644
--- a/src/com/android/contacts/activities/PhotoSelectionActivity.java
+++ b/src/com/android/contacts/activities/PhotoSelectionActivity.java
@@ -459,17 +459,21 @@
             }
 
             @Override
-            public void startPickFromGalleryActivity(Intent intent, int requestCode) {
+            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(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);
+                        ContactEditorActivity.ACTION_SAVE_COMPLETED, rawContactId, filePath);
                 startService(intent);
                 finish();
             }
diff --git a/src/com/android/contacts/detail/ContactDetailFragment.java b/src/com/android/contacts/detail/ContactDetailFragment.java
index beee359..7c79a4c 100644
--- a/src/com/android/contacts/detail/ContactDetailFragment.java
+++ b/src/com/android/contacts/detail/ContactDetailFragment.java
@@ -2000,7 +2000,7 @@
             // should update the ui
             final Intent intent = ContactSaveService.createSaveContactIntent(getActivity(),
                     contactDeltaList, "", 0, false, getActivity().getClass(),
-                    Intent.ACTION_VIEW);
+                    Intent.ACTION_VIEW, null);
             getActivity().startService(intent);
         }
     }
diff --git a/src/com/android/contacts/detail/PhotoSelectionHandler.java b/src/com/android/contacts/detail/PhotoSelectionHandler.java
index 72397c0..1423c65 100644
--- a/src/com/android/contacts/detail/PhotoSelectionHandler.java
+++ b/src/com/android/contacts/detail/PhotoSelectionHandler.java
@@ -32,6 +32,7 @@
 import android.content.Intent;
 import android.database.Cursor;
 import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
 import android.media.MediaScannerConnection;
 import android.net.Uri;
 import android.os.Environment;
@@ -131,7 +132,8 @@
         if (resultCode == Activity.RESULT_OK) {
             switch (requestCode) {
                 case REQUEST_CODE_PHOTO_PICKED_WITH_DATA: {
-                    Bitmap bitmap = data.getParcelableExtra("data");
+                    Bitmap bitmap = BitmapFactory.decodeFile(
+                            mListener.getCurrentPhotoFile().getAbsolutePath());
                     mListener.onPhotoSelected(bitmap);
                     return true;
                 }
@@ -172,6 +174,16 @@
     }
 
     /**
+     * Return the raw-contact id of the first entity in the contact data that belongs to a
+     * contact-writable account, or -1 if no such entity exists.
+     */
+    protected long getWritableEntityId() {
+        int index = getWritableEntityIndex();
+        if (index == -1) return -1;
+        return mState.get(index).getValues().getId();
+    }
+
+    /**
      * 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.
@@ -180,28 +192,14 @@
      *     or null if the photo could not be parsed or none of the accounts associated with the
      *     contact are writable.
      */
-    public EntityDeltaList getDeltaForAttachingPhotoToContact(Bitmap bitmap) {
+    public EntityDeltaList getDeltaForAttachingPhotoToContact() {
         // Find the first writable entity.
         int writableEntityIndex = getWritableEntityIndex();
         if (writableEntityIndex != -1) {
-            // Convert the photo to a byte array.
-            final int size = bitmap.getWidth() * bitmap.getHeight() * 4;
-            final ByteArrayOutputStream out = new ByteArrayOutputStream(size);
-
-            try {
-                bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
-                out.flush();
-                out.close();
-            } catch (IOException e) {
-                Log.w(TAG, "Unable to serialize photo: " + e.toString());
-                return null;
-            }
-
             // Note - guaranteed to have contact data if we have a writable entity index.
             EntityDelta delta = mState.get(writableEntityIndex);
             ValuesDelta child = EntityModifier.ensureKindExists(
                     delta, mWritableAccount, Photo.CONTENT_ITEM_TYPE);
-            child.put(Photo.PHOTO, out.toByteArray());
             child.setFromTemplate(false);
             child.put(Photo.IS_SUPER_PRIMARY, 1);
 
@@ -223,8 +221,8 @@
                     null);
 
             // Launch gallery to crop the photo
-            final Intent intent = getCropImageIntent(Uri.fromFile(f));
-            mListener.startPickFromGalleryActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA);
+            final Intent intent = getCropImageIntent(f);
+            mListener.startPickFromGalleryActivity(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();
@@ -237,6 +235,11 @@
         return dateFormat.format(date) + ".jpg";
     }
 
+    private File getPhotoFile() {
+        PHOTO_DIR.mkdirs();
+        return new File(PHOTO_DIR, getPhotoFileName());
+    }
+
     private int getPhotoPickSize() {
         // Note that this URI is safe to call on the UI thread.
         Cursor c = mContext.getContentResolver().query(DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI,
@@ -252,7 +255,8 @@
     /**
      * Constructs an intent for picking a photo from Gallery, cropping it and returning the bitmap.
      */
-    private Intent getPhotoPickIntent() {
+    private Intent getPhotoPickIntent(File photoFile) {
+        Uri photoUri = Uri.fromFile(photoFile);
         Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
         intent.setType("image/*");
         intent.putExtra("crop", "true");
@@ -260,14 +264,15 @@
         intent.putExtra("aspectY", 1);
         intent.putExtra("outputX", mPhotoPickSize);
         intent.putExtra("outputY", mPhotoPickSize);
-        intent.putExtra("return-data", true);
+        intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
         return intent;
     }
 
     /**
      * Constructs an intent for image cropping.
      */
-    private Intent getCropImageIntent(Uri photoUri) {
+    private Intent getCropImageIntent(File photoFile) {
+        Uri photoUri = Uri.fromFile(photoFile);
         Intent intent = new Intent("com.android.camera.action.CROP");
         intent.setDataAndType(photoUri, "image/*");
         intent.putExtra("crop", "true");
@@ -275,7 +280,7 @@
         intent.putExtra("aspectY", 1);
         intent.putExtra("outputX", mPhotoPickSize);
         intent.putExtra("outputY", mPhotoPickSize);
-        intent.putExtra("return-data", true);
+        intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
         return intent;
     }
 
@@ -303,10 +308,9 @@
         public void onTakePhotoChosen() {
             try {
                 // Launch camera to take photo for selected contact
-                PHOTO_DIR.mkdirs();
-                File photoFile = new File(PHOTO_DIR, getPhotoFileName());
-                startTakePhotoActivity(getTakePhotoIntent(photoFile),
-                        REQUEST_CODE_CAMERA_WITH_DATA, photoFile);
+                File f = getPhotoFile();
+                final Intent intent = getTakePhotoIntent(f);
+                startTakePhotoActivity(intent, REQUEST_CODE_CAMERA_WITH_DATA, f);
             } catch (ActivityNotFoundException e) {
                 Toast.makeText(mContext, R.string.photoPickerNotFoundText,
                         Toast.LENGTH_LONG).show();
@@ -317,8 +321,9 @@
         public void onPickFromGalleryChosen() {
             try {
                 // Launch picker to choose photo for selected contact
-                final Intent intent = getPhotoPickIntent();
-                startPickFromGalleryActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA);
+                File f = getPhotoFile();
+                final Intent intent = getPhotoPickIntent(f);
+                startPickFromGalleryActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA, f);
             } catch (ActivityNotFoundException e) {
                 Toast.makeText(mContext, R.string.photoPickerNotFoundText,
                         Toast.LENGTH_LONG).show();
@@ -340,9 +345,12 @@
          * 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);
+        public abstract void startPickFromGalleryActivity(Intent intent, int requestCode,
+                File photoFile);
 
         /**
          * Called when the user has completed selection of a photo.
diff --git a/src/com/android/contacts/editor/ContactEditorFragment.java b/src/com/android/contacts/editor/ContactEditorFragment.java
index 97b1c47..2ede872 100644
--- a/src/com/android/contacts/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorFragment.java
@@ -188,9 +188,7 @@
     private static final int REQUEST_CODE_JOIN = 0;
     private static final int REQUEST_CODE_ACCOUNTS_CHANGED = 1;
 
-    private Bitmap mPhoto = null;
     private long mRawContactIdRequestingPhoto = -1;
-    private long mRawContactIdRequestingPhotoAfterLoad = -1;
     private PhotoSelectionHandler mPhotoSelectionHandler;
 
     private final EntityDeltaComparator mComparator = new EntityDeltaComparator();
@@ -198,6 +196,7 @@
     private Cursor mGroupMetaData;
 
     private File mCurrentPhotoFile;
+    private final Bundle mUpdatedPhotos = new Bundle();
 
     private Context mContext;
     private String mAction;
@@ -975,8 +974,9 @@
 
         // Save contact
         Intent intent = ContactSaveService.createSaveContactIntent(getActivity(), mState,
-                SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(),
-                getActivity().getClass(), ContactEditorActivity.ACTION_SAVE_COMPLETED);
+                SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(), getActivity().getClass(),
+                ContactEditorActivity.ACTION_SAVE_COMPLETED, mUpdatedPhotos);
+
         getActivity().startService(intent);
         return true;
     }
@@ -1564,13 +1564,21 @@
     /**
      * Sets the photo stored in mPhoto and writes it to the RawContact with the given id
      */
-    private void setPhoto(long rawContact, Bitmap photo) {
+    private void setPhoto(long rawContact, Bitmap photo, File photoFile) {
         BaseRawContactEditorView requestingEditor = getRawContactEditorView(rawContact);
+
+        if (photo == null || photo.getHeight() < 0 || photo.getWidth() < 0) {
+            // This is unexpected.
+            Log.w(TAG, "Invalid bitmap passed to setPhoto()");
+        }
+
         if (requestingEditor != null) {
             requestingEditor.setPhotoBitmap(photo);
         } else {
             Log.w(TAG, "The contact that requested the photo is no longer present.");
         }
+
+        mUpdatedPhotos.putString(String.valueOf(rawContact), photoFile.getAbsolutePath());
     }
 
     /**
@@ -1637,12 +1645,6 @@
             setData(data);
             final long setDataEndTime = SystemClock.elapsedRealtime();
 
-            // If we are coming back from the photo trimmer, this will be set.
-            if (mRawContactIdRequestingPhotoAfterLoad != -1) {
-                setPhoto(mRawContactIdRequestingPhotoAfterLoad, mPhoto);
-                mRawContactIdRequestingPhotoAfterLoad = -1;
-                mPhoto = null;
-            }
             Log.v(TAG, "Time needed for setting UI: " + (setDataEndTime-setDataStartTime));
         }
 
@@ -1757,16 +1759,17 @@
             }
 
             @Override
-            public void startPickFromGalleryActivity(Intent intent, int requestCode) {
+            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);
-                mRawContactIdRequestingPhotoAfterLoad = mRawContactIdRequestingPhoto;
+                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 086b07f..db29544 100644
--- a/src/com/android/contacts/editor/PhotoEditorView.java
+++ b/src/com/android/contacts/editor/PhotoEditorView.java
@@ -132,25 +132,13 @@
             return;
         }
 
-        final int size = photo.getWidth() * photo.getHeight() * 4;
-        final ByteArrayOutputStream out = new ByteArrayOutputStream(size);
+        mPhotoImageView.setImageBitmap(photo);
+        mFrameView.setEnabled(isEnabled());
+        mHasSetPhoto = true;
+        mEntry.setFromTemplate(false);
 
-        try {
-            photo.compress(Bitmap.CompressFormat.PNG, 100, out);
-            out.flush();
-            out.close();
-
-            mEntry.put(Photo.PHOTO, out.toByteArray());
-            mPhotoImageView.setImageBitmap(photo);
-            mFrameView.setEnabled(isEnabled());
-            mHasSetPhoto = true;
-            mEntry.setFromTemplate(false);
-
-            // When the user chooses a new photo mark it as super primary
-            mEntry.put(Photo.IS_SUPER_PRIMARY, 1);
-        } catch (IOException e) {
-            Log.w(TAG, "Unable to serialize photo: " + e.toString());
-        }
+        // When the user chooses a new photo mark it as super primary
+        mEntry.put(Photo.IS_SUPER_PRIMARY, 1);
     }
 
     /**