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/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.