Make contacts photo pickers compatible with new documents UI

The old contacts photo picker code was using unguaranteed behavior
(that Intent.GET_CONTENT would support MediaStore.EXTRA_OUTPUT) and this
caused it to not work anymore with the new document picker.

This CL changes all usages of files to instead use URIs.

Also, a FileProvider has been added to Contacts, to allow us to pass in
URI pointing to our private cache in intent.setClipData with
Intent.FLAG_GRANT_WRITE_URI_PERMISSION and Intent.FLAG_GRANT_READ_URI_PERMISSION
so we no longer have to reply on the MediaStore.EXTRA_OUTPUT being parsed
and supported. The use of the FileProvider also prevents unauthorized access
to temporary files during the caching process.

Bug: 10745342

Change-Id: Iaee3d7d112dd124a2f5596c4b9704ea75d3b3419
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index 32fb190..8a3ef32 100644
--- a/src/com/android/contacts/ContactSaveService.java
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -54,6 +54,8 @@
 import com.android.contacts.model.RawContactModifier;
 import com.android.contacts.common.model.account.AccountWithDataSet;
 import com.android.contacts.util.CallerInfoCacheUtils;
+import com.android.contacts.util.ContactPhotoUtils;
+
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 
@@ -61,6 +63,7 @@
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -294,9 +297,9 @@
     public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
             String saveModeExtraKey, int saveMode, boolean isProfile,
             Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
-            String updatedPhotoPath) {
+            Uri updatedPhotoPath) {
         Bundle bundle = new Bundle();
-        bundle.putString(String.valueOf(rawContactId), updatedPhotoPath);
+        bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
         return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
                 callbackActivity, callbackAction, bundle);
     }
@@ -449,7 +452,7 @@
         // the ContactProvider already knows about newly-created contacts.
         if (updatedPhotos != null) {
             for (String key : updatedPhotos.keySet()) {
-                String photoFilePath = updatedPhotos.getString(key);
+                Uri photoUri = updatedPhotos.getParcelable(key);
                 long rawContactId = Long.parseLong(key);
 
                 // If the raw-contact ID is negative, we are saving a new raw-contact;
@@ -462,8 +465,7 @@
                     }
                 }
 
-                File photoFile = new File(photoFilePath);
-                if (!saveUpdatedPhoto(rawContactId, photoFile)) succeeded = false;
+                if (!saveUpdatedPhoto(rawContactId, photoUri)) succeeded = false;
             }
         }
 
@@ -484,37 +486,12 @@
      * Save updated photo for the specified raw-contact.
      * @return true for success, false for failure
      */
-    private boolean saveUpdatedPhoto(long rawContactId, File photoFile) {
+    private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri) {
         final Uri outputUri = Uri.withAppendedPath(
                 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
                 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
 
-        try {
-            final FileOutputStream outputStream = getContentResolver()
-                    .openAssetFileDescriptor(outputUri, "rw").createOutputStream();
-            try {
-                final FileInputStream inputStream = new FileInputStream(photoFile);
-                try {
-                    final byte[] buffer = new byte[16 * 1024];
-                    int length;
-                    int totalLength = 0;
-                    while ((length = inputStream.read(buffer)) > 0) {
-                        outputStream.write(buffer, 0, length);
-                        totalLength += length;
-                    }
-                    Log.v(TAG, "Wrote " + totalLength + " bytes for photo " + photoFile.toString());
-                } finally {
-                    inputStream.close();
-                }
-            } finally {
-                outputStream.close();
-                photoFile.delete();
-            }
-        } catch (IOException e) {
-            Log.e(TAG, "Failed to write photo: " + photoFile.toString() + " because: " + e);
-            return false;
-        }
-        return true;
+        return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, true);
     }
 
     /**
diff --git a/src/com/android/contacts/activities/AttachPhotoActivity.java b/src/com/android/contacts/activities/AttachPhotoActivity.java
index c0a751c..3239f55 100644
--- a/src/com/android/contacts/activities/AttachPhotoActivity.java
+++ b/src/com/android/contacts/activities/AttachPhotoActivity.java
@@ -43,6 +43,7 @@
 import com.android.contacts.util.ContactPhotoUtils;
 
 import java.io.File;
+import java.io.FileNotFoundException;
 
 /**
  * Provides an external interface for other applications to attach images
@@ -59,7 +60,6 @@
     private static final String KEY_CONTACT_URI = "contact_uri";
     private static final String KEY_TEMP_PHOTO_URI = "temp_photo_uri";
 
-    private File mTempPhotoFile;
     private Uri mTempPhotoUri;
 
     private ContentResolver mContentResolver;
@@ -76,13 +76,9 @@
         if (icicle != null) {
             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(this);
-            mTempPhotoUri = Uri.fromFile(mTempPhotoFile);
-
+            mTempPhotoUri = ContactPhotoUtils.generateTempImageUri(this);
             Intent intent = new Intent(Intent.ACTION_PICK);
             intent.setType(Contacts.CONTENT_TYPE);
             startActivityForResult(intent, REQUEST_PICK_CONTACT);
@@ -104,8 +100,12 @@
     @Override
     protected void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
-        if (mContactUri != null) outState.putString(KEY_CONTACT_URI, mContactUri.toString());
-        outState.putString(KEY_TEMP_PHOTO_URI, mTempPhotoUri.toString());
+        if (mContactUri != null) {
+            outState.putString(KEY_CONTACT_URI, mContactUri.toString());
+        }
+        if (mTempPhotoUri != null) {
+            outState.putString(KEY_TEMP_PHOTO_URI, mTempPhotoUri.toString());
+        }
     }
 
     @Override
@@ -123,7 +123,9 @@
             if (myIntent.getStringExtra("mimeType") != null) {
                 intent.setDataAndType(myIntent.getData(), myIntent.getStringExtra("mimeType"));
             }
-            ContactPhotoUtils.addGalleryIntentExtras(intent, mTempPhotoUri, mPhotoDim);
+
+            ContactPhotoUtils.addPhotoPickerExtras(intent, mTempPhotoUri);
+            ContactPhotoUtils.addCropExtras(intent, mPhotoDim);
 
             startActivityForResult(intent, REQUEST_CROP_PHOTO);
 
@@ -183,14 +185,20 @@
 
         // 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());
+        Bitmap bitmap;
+        try {
+            bitmap = ContactPhotoUtils.getBitmapFromUri(this, mTempPhotoUri);
+        } catch (FileNotFoundException e) {
+            Log.w(TAG, "Could not find bitmap");
+            return;
+        }
+
         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;
         }
-
         // 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
@@ -213,7 +221,8 @@
                 contact.isUserProfile(),
                 null, null,
                 raw.getRawContactId(),
-                mTempPhotoFile.getAbsolutePath());
+                mTempPhotoUri
+                );
         startService(intent);
         finish();
     }
diff --git a/src/com/android/contacts/activities/PhotoSelectionActivity.java b/src/com/android/contacts/activities/PhotoSelectionActivity.java
index 3b1032f..6d74863 100644
--- a/src/com/android/contacts/activities/PhotoSelectionActivity.java
+++ b/src/com/android/contacts/activities/PhotoSelectionActivity.java
@@ -28,6 +28,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Parcelable;
+import android.support.v4.content.FileProvider;
 import android.view.View;
 import android.view.ViewGroup.MarginLayoutParams;
 import android.widget.FrameLayout.LayoutParams;
@@ -42,6 +43,9 @@
 import com.android.contacts.util.ContactPhotoUtils;
 import com.android.contacts.util.SchedulingUtils;
 
+import java.io.File;
+import java.io.FileNotFoundException;
+
 
 /**
  * Popup activity for choosing a contact photo within the Contacts app.
@@ -59,8 +63,8 @@
     /** Number of ms for the animation to hide the backdrop on finish. */
     private static final int BACKDROP_FADEOUT_DURATION = 100;
 
-    /** Key used to persist photo-filename (NOT full file-path). */
-    private static final String KEY_CURRENT_PHOTO_FILE = "currentphotofile";
+    /** Key used to persist photo uri. */
+    private static final String KEY_CURRENT_PHOTO_URI = "currentphotouri";
 
     /** Key used to persist whether a sub-activity is currently in progress. */
     private static final String KEY_SUB_ACTIVITY_IN_PROGRESS = "subinprogress";
@@ -151,16 +155,16 @@
     private PendingPhotoResult mPendingPhotoResult;
 
     /**
-     * The photo file being interacted with, if any.  Saved/restored between activity instances.
+     * The photo uri being interacted with, if any.  Saved/restored between activity instances.
      */
-    private String mCurrentPhotoFile;
+    private Uri mCurrentPhotoUri;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.photoselection_activity);
         if (savedInstanceState != null) {
-            mCurrentPhotoFile = savedInstanceState.getString(KEY_CURRENT_PHOTO_FILE);
+            mCurrentPhotoUri = savedInstanceState.getParcelable(KEY_CURRENT_PHOTO_URI);
             mSubActivityInProgress = savedInstanceState.getBoolean(KEY_SUB_ACTIVITY_IN_PROGRESS);
         }
 
@@ -456,7 +460,7 @@
     @Override
     protected void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
-        outState.putString(KEY_CURRENT_PHOTO_FILE, mCurrentPhotoFile);
+        outState.putParcelable(KEY_CURRENT_PHOTO_URI, mCurrentPhotoUri);
         outState.putBoolean(KEY_SUB_ACTIVITY_IN_PROGRESS, mSubActivityInProgress);
     }
 
@@ -527,28 +531,27 @@
         }
 
         @Override
-        public void startPhotoActivity(Intent intent, int requestCode, String photoFile) {
+        public void startPhotoActivity(Intent intent, int requestCode, Uri photoUri) {
             mSubActivityInProgress = true;
-            mCurrentPhotoFile = photoFile;
+            mCurrentPhotoUri = photoUri;
             PhotoSelectionActivity.this.startActivityForResult(intent, requestCode);
         }
 
         private final class PhotoListener extends PhotoActionListener {
             @Override
-            public void onPhotoSelected(Bitmap bitmap) {
+            public void onPhotoSelected(Uri uri) {
                 RawContactDeltaList delta = getDeltaForAttachingPhotoToContact();
                 long rawContactId = getWritableEntityId();
-                final String croppedPath = ContactPhotoUtils.pathForCroppedPhoto(
-                        PhotoSelectionActivity.this, mCurrentPhotoFile);
+
                 Intent intent = ContactSaveService.createSaveContactIntent(
-                        mContext, delta, "", 0, mIsProfile, null, null, rawContactId, croppedPath);
+                        mContext, delta, "", 0, mIsProfile, null, null, rawContactId, uri);
                 startService(intent);
                 finish();
             }
 
             @Override
-            public String getCurrentPhotoFile() {
-                return mCurrentPhotoFile;
+            public Uri getCurrentPhotoUri() {
+                return mCurrentPhotoUri;
             }
 
             @Override
diff --git a/src/com/android/contacts/detail/PhotoSelectionHandler.java b/src/com/android/contacts/detail/PhotoSelectionHandler.java
index 9689acc..6e2d4fa 100644
--- a/src/com/android/contacts/detail/PhotoSelectionHandler.java
+++ b/src/com/android/contacts/detail/PhotoSelectionHandler.java
@@ -22,9 +22,6 @@
 import android.content.Context;
 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.provider.ContactsContract.CommonDataKinds.Photo;
 import android.provider.ContactsContract.DisplayPhoto;
@@ -48,7 +45,7 @@
 import com.android.contacts.util.ContactPhotoUtils;
 import com.android.contacts.util.UiClosables;
 
-import java.io.File;
+import java.io.FileNotFoundException;
 
 /**
  * Handles displaying a photo selection popup for a given photo view and dealing with the results
@@ -60,11 +57,14 @@
 
     private static final int REQUEST_CODE_CAMERA_WITH_DATA = 1001;
     private static final int REQUEST_CODE_PHOTO_PICKED_WITH_DATA = 1002;
+    private static final int REQUEST_CROP_PHOTO = 1003;
 
     protected final Context mContext;
     private final View mPhotoView;
     private final int mPhotoMode;
     private final int mPhotoPickSize;
+    private final Uri mCroppedPhotoUri;
+    private final Uri mTempPhotoUri;
     private final RawContactDeltaList mState;
     private final boolean mIsDirectoryContact;
     private ListPopupWindow mPopup;
@@ -74,6 +74,8 @@
         mContext = context;
         mPhotoView = photoView;
         mPhotoMode = photoMode;
+        mTempPhotoUri = ContactPhotoUtils.generateTempImageUri(context);
+        mCroppedPhotoUri = ContactPhotoUtils.generateTempCroppedImageUri(mContext);
         mIsDirectoryContact = isDirectoryContact;
         mState = state;
         mPhotoPickSize = getPhotoPickSize();
@@ -115,19 +117,55 @@
         final PhotoActionListener listener = getListener();
         if (resultCode == Activity.RESULT_OK) {
             switch (requestCode) {
-                // Photo was chosen (either new or existing from gallery), and cropped.
-                case REQUEST_CODE_PHOTO_PICKED_WITH_DATA: {
-                    final String path = ContactPhotoUtils.pathForCroppedPhoto(
-                            mContext, listener.getCurrentPhotoFile());
-                    Bitmap bitmap = BitmapFactory.decodeFile(path);
-                    listener.onPhotoSelected(bitmap);
-                    return true;
+                // Cropped photo was returned
+                case REQUEST_CROP_PHOTO: {
+                    final Uri uri;
+                    if (data != null && data.getData() != null) {
+                        uri = data.getData();
+                    } else {
+                        uri = mCroppedPhotoUri;
+                    }
+
+                    try {
+                        // delete the original temporary photo if it exists
+                        mContext.getContentResolver().delete(mTempPhotoUri, null, null);
+                        listener.onPhotoSelected(uri);
+                        return true;
+                    } catch (FileNotFoundException e) {
+                        return false;
+                    }
                 }
-                // Photo was successfully taken, now crop it.
-                case REQUEST_CODE_CAMERA_WITH_DATA: {
-                    doCropPhoto(listener.getCurrentPhotoFile());
+
+                // Photo was successfully taken or selected from gallery, now crop it.
+                case REQUEST_CODE_PHOTO_PICKED_WITH_DATA:
+                case REQUEST_CODE_CAMERA_WITH_DATA:
+                    final Uri uri;
+                    boolean isWritable = false;
+                    if (data != null && data.getData() != null) {
+                        uri = data.getData();
+                    } else {
+                        uri = listener.getCurrentPhotoUri();
+                        isWritable = true;
+                    }
+                    final Uri toCrop;
+                    if (isWritable) {
+                        // Since this uri belongs to our file provider, we know that it is writable
+                        // by us. This means that we don't have to save it into another temporary
+                        // location just to be able to crop it.
+                        toCrop = uri;
+                    } else {
+                        toCrop = mTempPhotoUri;
+                        try {
+                            ContactPhotoUtils.savePhotoFromUriToUri(mContext, uri,
+                                    toCrop, false);
+                        } catch (SecurityException e) {
+                            Log.d(TAG, "Did not have read-access to uri : " + uri);
+                            return false;
+                        }
+                    }
+
+                    doCropPhoto(toCrop, mCroppedPhotoUri);
                     return true;
-                }
             }
         }
         return false;
@@ -186,28 +224,16 @@
     }
 
     /** Used by subclasses to delegate to their enclosing Activity or Fragment. */
-    protected abstract void startPhotoActivity(Intent intent, int requestCode, String photoFile);
+    protected abstract void startPhotoActivity(Intent intent, int requestCode, Uri photoUri);
 
     /**
      * Sends a newly acquired photo to Gallery for cropping
      */
-    private void doCropPhoto(String fileName) {
+    private void doCropPhoto(Uri inputUri, Uri outputUri) {
         try {
-            // Obtain the absolute paths for the newly-taken photo, and the destination
-            // for the soon-to-be-cropped photo.
-            final String newPath = ContactPhotoUtils.pathForNewCameraPhoto(fileName);
-            final String croppedPath = ContactPhotoUtils.pathForCroppedPhoto(mContext, fileName);
-
-            // Add the image to the media store
-            MediaScannerConnection.scanFile(
-                    mContext,
-                    new String[] { newPath },
-                    new String[] { null },
-                    null);
-
             // Launch gallery to crop the photo
-            final Intent intent = getCropImageIntent(newPath, croppedPath);
-            startPhotoActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA, fileName);
+            final Intent intent = getCropImageIntent(inputUri, outputUri);
+            startPhotoActivity(intent, REQUEST_CROP_PHOTO, inputUri);
         } catch (Exception e) {
             Log.e(TAG, "Cannot crop image", e);
             Toast.makeText(mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
@@ -220,9 +246,9 @@
      *     what should be returned by
      *     {@link PhotoSelectionHandler.PhotoActionListener#getCurrentPhotoFile()}.
      */
-    private void startTakePhotoActivity(String photoFile) {
-        final Intent intent = getTakePhotoIntent(photoFile);
-        startPhotoActivity(intent, REQUEST_CODE_CAMERA_WITH_DATA, photoFile);
+    private void startTakePhotoActivity(Uri photoUri) {
+        final Intent intent = getTakePhotoIntent(photoUri);
+        startPhotoActivity(intent, REQUEST_CODE_CAMERA_WITH_DATA, photoUri);
     }
 
     /**
@@ -231,9 +257,9 @@
      *     stored by the content-provider.
      *     {@link PhotoSelectionHandler#handlePhotoActivityResult(int, int, Intent)}.
      */
-    private void startPickFromGalleryActivity(String photoFile) {
-        final Intent intent = getPhotoPickIntent(photoFile);
-        startPhotoActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA, photoFile);
+    private void startPickFromGalleryActivity(Uri photoUri) {
+        final Intent intent = getPhotoPickIntent(photoUri);
+        startPhotoActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA, photoUri);
     }
 
     private int getPhotoPickSize() {
@@ -249,36 +275,32 @@
     }
 
     /**
-     * Constructs an intent for picking a photo from Gallery, cropping it and returning the bitmap.
+     * Constructs an intent for capturing a photo and storing it in a temporary output uri.
      */
-    private Intent getPhotoPickIntent(String photoFile) {
-        final String croppedPhotoPath = ContactPhotoUtils.pathForCroppedPhoto(mContext, photoFile);
-        final Uri croppedPhotoUri = Uri.fromFile(new File(croppedPhotoPath));
+    private Intent getTakePhotoIntent(Uri outputUri) {
+        final Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE, null);
+        ContactPhotoUtils.addPhotoPickerExtras(intent, outputUri);
+        return intent;
+    }
+
+    /**
+     * Constructs an intent for picking a photo from Gallery, and returning the bitmap.
+     */
+    private Intent getPhotoPickIntent(Uri outputUri) {
         final Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
         intent.setType("image/*");
-        ContactPhotoUtils.addGalleryIntentExtras(intent, croppedPhotoUri, mPhotoPickSize);
+        ContactPhotoUtils.addPhotoPickerExtras(intent, outputUri);
         return intent;
     }
 
     /**
      * Constructs an intent for image cropping.
      */
-    private Intent getCropImageIntent(String inputPhotoPath, String croppedPhotoPath) {
-        final Uri inputPhotoUri = Uri.fromFile(new File(inputPhotoPath));
-        final Uri croppedPhotoUri = Uri.fromFile(new File(croppedPhotoPath));
+    private Intent getCropImageIntent(Uri inputUri, Uri outputUri) {
         Intent intent = new Intent("com.android.camera.action.CROP");
-        intent.setDataAndType(inputPhotoUri, "image/*");
-        ContactPhotoUtils.addGalleryIntentExtras(intent, croppedPhotoUri, mPhotoPickSize);
-        return intent;
-    }
-
-    /**
-     * Constructs an intent for capturing a photo and storing it in a temporary file.
-     */
-    private static Intent getTakePhotoIntent(String fileName) {
-        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE, null);
-        final String newPhotoPath = ContactPhotoUtils.pathForNewCameraPhoto(fileName);
-        intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File(newPhotoPath)));
+        intent.setDataAndType(inputUri, "image/*");
+        ContactPhotoUtils.addPhotoPickerExtras(intent, outputUri);
+        ContactPhotoUtils.addCropExtras(intent, mPhotoPickSize);
         return intent;
     }
 
@@ -297,7 +319,7 @@
         public void onTakePhotoChosen() {
             try {
                 // Launch camera to take photo for selected contact
-                startTakePhotoActivity(ContactPhotoUtils.generateTempPhotoFileName());
+                startTakePhotoActivity(mTempPhotoUri);
             } catch (ActivityNotFoundException e) {
                 Toast.makeText(
                         mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
@@ -308,7 +330,7 @@
         public void onPickFromGalleryChosen() {
             try {
                 // Launch picker to choose photo for selected contact
-                startPickFromGalleryActivity(ContactPhotoUtils.generateTempPhotoFileName());
+                startPickFromGalleryActivity(mTempPhotoUri);
             } catch (ActivityNotFoundException e) {
                 Toast.makeText(
                         mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
@@ -317,16 +339,16 @@
 
         /**
          * Called when the user has completed selection of a photo.
-         * @param bitmap The selected and cropped photo.
+         * @throws FileNotFoundException
          */
-        public abstract void onPhotoSelected(Bitmap bitmap);
+        public abstract void onPhotoSelected(Uri uri) throws FileNotFoundException;
 
         /**
          * Gets the current photo file that is being interacted with.  It is the activity or
          * fragment's responsibility to maintain this in saved state, since this handler instance
          * will not survive rotation.
          */
-        public abstract String getCurrentPhotoFile();
+        public abstract Uri getCurrentPhotoUri();
 
         /**
          * Called when the photo selection dialog is dismissed.
diff --git a/src/com/android/contacts/editor/ContactEditorFragment.java b/src/com/android/contacts/editor/ContactEditorFragment.java
index 27c1498..767cee2 100644
--- a/src/com/android/contacts/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorFragment.java
@@ -91,6 +91,7 @@
 import com.google.common.collect.Lists;
 
 import java.io.File;
+import java.io.FileNotFoundException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -111,7 +112,7 @@
     private static final String KEY_EDIT_STATE = "state";
     private static final String KEY_RAW_CONTACT_ID_REQUESTING_PHOTO = "photorequester";
     private static final String KEY_VIEW_ID_GENERATOR = "viewidgenerator";
-    private static final String KEY_CURRENT_PHOTO_FILE = "currentphotofile";
+    private static final String KEY_CURRENT_PHOTO_URI = "currentphotouri";
     private static final String KEY_CONTACT_ID_FOR_JOIN = "contactidforjoin";
     private static final String KEY_CONTACT_WRITABLE_FOR_JOIN = "contactwritableforjoin";
     private static final String KEY_SHOW_JOIN_SUGGESTIONS = "showJoinSuggestions";
@@ -220,7 +221,7 @@
 
     private Cursor mGroupMetaData;
 
-    private String mCurrentPhotoFile;
+    private Uri mCurrentPhotoUri;
     private Bundle mUpdatedPhotos = new Bundle();
 
     private Context mContext;
@@ -482,7 +483,7 @@
             mRawContactIdRequestingPhoto = savedState.getLong(
                     KEY_RAW_CONTACT_ID_REQUESTING_PHOTO);
             mViewIdGenerator = savedState.getParcelable(KEY_VIEW_ID_GENERATOR);
-            mCurrentPhotoFile = savedState.getString(KEY_CURRENT_PHOTO_FILE);
+            mCurrentPhotoUri = savedState.getParcelable(KEY_CURRENT_PHOTO_URI);
             mContactIdForJoin = savedState.getLong(KEY_CONTACT_ID_FOR_JOIN);
             mContactWritableForJoin = savedState.getBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN);
             mAggregationSuggestionsRawContactId = savedState.getLong(KEY_SHOW_JOIN_SUGGESTIONS);
@@ -1654,7 +1655,7 @@
         }
         outState.putLong(KEY_RAW_CONTACT_ID_REQUESTING_PHOTO, mRawContactIdRequestingPhoto);
         outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator);
-        outState.putString(KEY_CURRENT_PHOTO_FILE, mCurrentPhotoFile);
+        outState.putParcelable(KEY_CURRENT_PHOTO_URI, mCurrentPhotoUri);
         outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin);
         outState.putBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN, mContactWritableForJoin);
         outState.putLong(KEY_SHOW_JOIN_SUGGESTIONS, mAggregationSuggestionsRawContactId);
@@ -1722,7 +1723,7 @@
     /**
      * Sets the photo stored in mPhoto and writes it to the RawContact with the given id
      */
-    private void setPhoto(long rawContact, Bitmap photo, String photoFile) {
+    private void setPhoto(long rawContact, Bitmap photo, Uri photoUri) {
         BaseRawContactEditorView requestingEditor = getRawContactEditorView(rawContact);
 
         if (photo == null || photo.getHeight() < 0 || photo.getWidth() < 0) {
@@ -1736,9 +1737,7 @@
             Log.w(TAG, "The contact that requested the photo is no longer present.");
         }
 
-        final String croppedPhotoPath =
-                ContactPhotoUtils.pathForCroppedPhoto(mContext, mCurrentPhotoFile);
-        mUpdatedPhotos.putString(String.valueOf(rawContact), croppedPhotoPath);
+        mUpdatedPhotos.putParcelable(String.valueOf(rawContact), photoUri);
     }
 
     /**
@@ -1771,11 +1770,12 @@
                     countWithPicture++;
                 } else {
                     final long rawContactId = entity.getRawContactId();
-                    final String path = mUpdatedPhotos.getString(String.valueOf(rawContactId));
-                    if (path != null) {
-                        final File file = new File(path);
-                        if (file.exists()) {
+                    final Uri uri = mUpdatedPhotos.getParcelable(String.valueOf(rawContactId));
+                    if (uri != null) {
+                        try {
+                            mContext.getContentResolver().openInputStream(uri);
                             countWithPicture++;
+                        } catch (FileNotFoundException e) {
                         }
                     }
                 }
@@ -1886,11 +1886,11 @@
         }
 
         @Override
-        public void startPhotoActivity(Intent intent, int requestCode, String photoFile) {
+        public void startPhotoActivity(Intent intent, int requestCode, Uri photoUri) {
             mRawContactIdRequestingPhoto = mEditor.getRawContactId();
             mCurrentPhotoHandler = this;
             mStatus = Status.SUB_ACTIVITY;
-            mCurrentPhotoFile = photoFile;
+            mCurrentPhotoUri = photoUri;
             ContactEditorFragment.this.startActivityForResult(intent, requestCode);
         }
 
@@ -1945,15 +1945,16 @@
             }
 
             @Override
-            public void onPhotoSelected(Bitmap bitmap) {
-                setPhoto(mRawContactId, bitmap, mCurrentPhotoFile);
+            public void onPhotoSelected(Uri uri) throws FileNotFoundException {
+                final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(mContext, uri);
+                setPhoto(mRawContactId, bitmap, uri);
                 mCurrentPhotoHandler = null;
                 bindEditors();
             }
 
             @Override
-            public String getCurrentPhotoFile() {
-                return mCurrentPhotoFile;
+            public Uri getCurrentPhotoUri() {
+                return mCurrentPhotoUri;
             }
 
             @Override
diff --git a/src/com/android/contacts/util/ContactPhotoUtils.java b/src/com/android/contacts/util/ContactPhotoUtils.java
index b14b36c..2b1c19a 100644
--- a/src/com/android/contacts/util/ContactPhotoUtils.java
+++ b/src/com/android/contacts/util/ContactPhotoUtils.java
@@ -17,17 +17,25 @@
 
 package com.android.contacts.util;
 
+import android.content.ClipData;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
 import android.net.Uri;
 import android.os.Environment;
 import android.provider.MediaStore;
+import android.support.v4.content.FileProvider;
 import android.util.Log;
 
+import com.google.common.io.Closeables;
+
 import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.Locale;
@@ -40,40 +48,57 @@
     private static final String TAG = "ContactPhotoUtils";
 
     private static final String PHOTO_DATE_FORMAT = "'IMG'_yyyyMMdd_HHmmss";
-    private static final String NEW_PHOTO_DIR_PATH =
-            Environment.getExternalStorageDirectory() + "/DCIM/Camera";
 
+    public static final String FILE_PROVIDER_AUTHORITY = "com.android.contacts.files";
 
     /**
      * 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.
+     * This file will be passed (as a uri) to other activities (such as the gallery/camera/
+     *  cropper/etc.), and read by us once they are finished writing it.
      */
-    public static File generateTempPhotoFile(Context context) {
-        return new File(pathForCroppedPhoto(context, generateTempPhotoFileName()));
+    public static Uri generateTempImageUri(Context context) {
+        return FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY,
+                new File(pathForTempPhoto(context, generateTempPhotoFileName())));
     }
 
-    public static String pathForCroppedPhoto(Context context, String fileName) {
-        final File dir = new File(context.getExternalCacheDir() + "/tmp");
+    public static Uri generateTempCroppedImageUri(Context context) {
+        return FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY,
+                new File(pathForTempPhoto(context, generateTempCroppedPhotoFileName())));
+    }
+
+    private static String pathForTempPhoto(Context context, String fileName) {
+        final File dir = context.getCacheDir();
         dir.mkdirs();
         final File f = new File(dir, fileName);
         return f.getAbsolutePath();
     }
 
-    public static String pathForNewCameraPhoto(String fileName) {
-        final File dir = new File(NEW_PHOTO_DIR_PATH);
-        dir.mkdirs();
-        final File f = new File(dir, fileName);
-        return f.getAbsolutePath();
-    }
-
-    public static String generateTempPhotoFileName() {
-        Date date = new Date(System.currentTimeMillis());
+    private static String generateTempPhotoFileName() {
+        final Date date = new Date(System.currentTimeMillis());
         SimpleDateFormat dateFormat = new SimpleDateFormat(PHOTO_DATE_FORMAT, Locale.US);
         return "ContactPhoto-" + dateFormat.format(date) + ".jpg";
     }
 
+    private static String generateTempCroppedPhotoFileName() {
+        final Date date = new Date(System.currentTimeMillis());
+        SimpleDateFormat dateFormat = new SimpleDateFormat(PHOTO_DATE_FORMAT, Locale.US);
+        return "ContactPhoto-" + dateFormat.format(date) + "-cropped.jpg";
+    }
+
+    /**
+     * Given a uri pointing to a bitmap, reads it into a bitmap and returns it.
+     * @throws FileNotFoundException
+     */
+    public static Bitmap getBitmapFromUri(Context context, Uri uri) throws FileNotFoundException {
+        final InputStream imageStream = context.getContentResolver().openInputStream(uri);
+        try {
+            return BitmapFactory.decodeStream(imageStream);
+        } finally {
+            Closeables.closeQuietly(imageStream);
+        }
+    }
+
     /**
      * Creates a byte[] containing the PNG-compressed bitmap, or null if
      * something goes wrong.
@@ -92,14 +117,7 @@
         }
     }
 
-    /**
-     * Adds common extras to gallery intents.
-     *
-     * @param intent The intent to add extras to.
-     * @param croppedPhotoUri The uri of the file to save the image to.
-     * @param photoSize The size of the photo to scale to.
-     */
-    public static void addGalleryIntentExtras(Intent intent, Uri croppedPhotoUri, int photoSize) {
+    public static void addCropExtras(Intent intent, int photoSize) {
         intent.putExtra("crop", "true");
         intent.putExtra("scale", true);
         intent.putExtra("scaleUpIfNeeded", true);
@@ -107,7 +125,53 @@
         intent.putExtra("aspectY", 1);
         intent.putExtra("outputX", photoSize);
         intent.putExtra("outputY", photoSize);
-        intent.putExtra(MediaStore.EXTRA_OUTPUT, croppedPhotoUri);
+    }
+
+    /**
+     * Adds common extras to gallery intents.
+     *
+     * @param intent The intent to add extras to.
+     * @param photoUri The uri of the file to save the image to.
+     */
+    public static void addPhotoPickerExtras(Intent intent, Uri photoUri) {
+        intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
+        intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
+                Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        intent.setClipData(ClipData.newRawUri(MediaStore.EXTRA_OUTPUT, photoUri));
+    }
+
+    /**
+     * Given an input photo stored in a uri, save it to a destination uri
+     */
+    public static boolean savePhotoFromUriToUri(Context context, Uri inputUri, Uri outputUri,
+            boolean deleteAfterSave) {
+        FileOutputStream outputStream = null;
+        InputStream inputStream = null;
+        try {
+            outputStream = context.getContentResolver()
+                    .openAssetFileDescriptor(outputUri, "rw").createOutputStream();
+            inputStream = context.getContentResolver().openInputStream(
+                    inputUri);
+
+            final byte[] buffer = new byte[16 * 1024];
+            int length;
+            int totalLength = 0;
+            while ((length = inputStream.read(buffer)) > 0) {
+                outputStream.write(buffer, 0, length);
+                totalLength += length;
+            }
+            Log.v(TAG, "Wrote " + totalLength + " bytes for photo " + inputUri.toString());
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to write photo: " + inputUri.toString() + " because: " + e);
+            return false;
+        } finally {
+            Closeables.closeQuietly(inputStream);
+            Closeables.closeQuietly(outputStream);
+            if (deleteAfterSave) {
+                context.getContentResolver().delete(inputUri, null, null);
+            }
+        }
+        return true;
     }
 }