diff options
author | Yorke Lee <yorkelee@google.com> | 2013-09-14 08:36:33 -0700 |
---|---|---|
committer | Steve Kondik <shade@chemlab.org> | 2013-12-08 19:45:29 -0800 |
commit | ca137e05e8f830d9c0d6be86bc5c801c4aefcc88 (patch) | |
tree | 4de94d21936409105571703d8ad7e6e192b908ed | |
parent | 78bba93db3df7209cc29708b39470e003f745bf6 (diff) | |
download | packages_apps_Contacts-ca137e05e8f830d9c0d6be86bc5c801c4aefcc88.tar.gz packages_apps_Contacts-ca137e05e8f830d9c0d6be86bc5c801c4aefcc88.tar.bz2 packages_apps_Contacts-ca137e05e8f830d9c0d6be86bc5c801c4aefcc88.zip |
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
-rw-r--r-- | AndroidManifest.xml | 10 | ||||
-rw-r--r-- | res/xml/file_paths.xml | 20 | ||||
-rw-r--r-- | src/com/android/contacts/ContactSaveService.java | 41 | ||||
-rw-r--r-- | src/com/android/contacts/activities/AttachPhotoActivity.java | 33 | ||||
-rw-r--r-- | src/com/android/contacts/activities/PhotoSelectionActivity.java | 31 | ||||
-rw-r--r-- | src/com/android/contacts/detail/PhotoSelectionHandler.java | 144 | ||||
-rw-r--r-- | src/com/android/contacts/editor/ContactEditorFragment.java | 37 | ||||
-rw-r--r-- | src/com/android/contacts/util/ContactPhotoUtils.java | 112 |
8 files changed, 267 insertions, 161 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 76bd152fb..0f64f0fa5 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -539,6 +539,16 @@ </intent-filter> </service> + <provider + android:name="android.support.v4.content.FileProvider" + android:authorities="com.android.contacts.files" + android:grantUriPermissions="true" + android:exported="false"> + <meta-data + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/file_paths" /> + </provider> + <meta-data android:name="android.nfc.disable_beam_default" android:value="true" /> </application> </manifest> diff --git a/res/xml/file_paths.xml b/res/xml/file_paths.xml new file mode 100644 index 000000000..294c0cbfc --- /dev/null +++ b/res/xml/file_paths.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2013 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. +--> + +<paths xmlns:android="http://schemas.android.com/apk/res/android"> + <!-- Offer access to files under Context.getCacheDir() --> + <cache-path name="my_cache" /> +</paths> diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java index 7c8782f76..55d78e71a 100644 --- a/src/com/android/contacts/ContactSaveService.java +++ b/src/com/android/contacts/ContactSaveService.java @@ -53,6 +53,8 @@ import com.android.contacts.model.RawContactDeltaList; 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; @@ -60,6 +62,7 @@ import java.io.File; 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; @@ -293,9 +296,9 @@ public class ContactSaveService extends IntentService { 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); } @@ -448,7 +451,7 @@ public class ContactSaveService extends IntentService { // 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; @@ -461,8 +464,7 @@ public class ContactSaveService extends IntentService { } } - File photoFile = new File(photoFilePath); - if (!saveUpdatedPhoto(rawContactId, photoFile)) succeeded = false; + if (!saveUpdatedPhoto(rawContactId, photoUri)) succeeded = false; } } @@ -483,37 +485,12 @@ public class ContactSaveService extends IntentService { * 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 c0a751c36..3239f55f1 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.common.model.ValuesDelta; 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 @@ public class AttachPhotoActivity extends ContactsActivity { 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 @@ public class AttachPhotoActivity extends ContactsActivity { 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 @@ public class AttachPhotoActivity extends ContactsActivity { @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 @@ public class AttachPhotoActivity extends ContactsActivity { 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 @@ public class AttachPhotoActivity extends ContactsActivity { // 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 @@ public class AttachPhotoActivity extends ContactsActivity { 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 3b1032f35..6d7486314 100644 --- a/src/com/android/contacts/activities/PhotoSelectionActivity.java +++ b/src/com/android/contacts/activities/PhotoSelectionActivity.java @@ -28,6 +28,7 @@ import android.graphics.Rect; 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.model.RawContactDeltaList; 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 @@ public class PhotoSelectionActivity extends Activity { /** 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 @@ public class PhotoSelectionActivity extends Activity { 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 @@ public class PhotoSelectionActivity extends Activity { @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 @@ public class PhotoSelectionActivity extends Activity { } @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 9689acc0e..6e2d4fa87 100644 --- a/src/com/android/contacts/detail/PhotoSelectionHandler.java +++ b/src/com/android/contacts/detail/PhotoSelectionHandler.java @@ -22,9 +22,6 @@ import android.content.ContentValues; 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.model.RawContactDeltaList; 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 @@ public abstract class PhotoSelectionHandler implements OnClickListener { 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 @@ public abstract class PhotoSelectionHandler implements OnClickListener { mContext = context; mPhotoView = photoView; mPhotoMode = photoMode; + mTempPhotoUri = ContactPhotoUtils.generateTempImageUri(context); + mCroppedPhotoUri = ContactPhotoUtils.generateTempCroppedImageUri(mContext); mIsDirectoryContact = isDirectoryContact; mState = state; mPhotoPickSize = getPhotoPickSize(); @@ -115,19 +117,55 @@ public abstract class PhotoSelectionHandler implements OnClickListener { 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 @@ public abstract class PhotoSelectionHandler implements OnClickListener { } /** 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 @@ public abstract class PhotoSelectionHandler implements OnClickListener { * 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 @@ public abstract class PhotoSelectionHandler implements OnClickListener { * 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 @@ public abstract class PhotoSelectionHandler implements OnClickListener { } /** - * 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)); - final Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null); - intent.setType("image/*"); - ContactPhotoUtils.addGalleryIntentExtras(intent, croppedPhotoUri, mPhotoPickSize); + 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 image cropping. + * Constructs an intent for picking a photo from Gallery, and returning the bitmap. */ - private Intent getCropImageIntent(String inputPhotoPath, String croppedPhotoPath) { - final Uri inputPhotoUri = Uri.fromFile(new File(inputPhotoPath)); - final Uri croppedPhotoUri = Uri.fromFile(new File(croppedPhotoPath)); - Intent intent = new Intent("com.android.camera.action.CROP"); - intent.setDataAndType(inputPhotoUri, "image/*"); - ContactPhotoUtils.addGalleryIntentExtras(intent, croppedPhotoUri, mPhotoPickSize); + private Intent getPhotoPickIntent(Uri outputUri) { + final Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null); + intent.setType("image/*"); + ContactPhotoUtils.addPhotoPickerExtras(intent, outputUri); return intent; } /** - * Constructs an intent for capturing a photo and storing it in a temporary file. + * Constructs an intent for image cropping. */ - 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))); + private Intent getCropImageIntent(Uri inputUri, Uri outputUri) { + Intent intent = new Intent("com.android.camera.action.CROP"); + intent.setDataAndType(inputUri, "image/*"); + ContactPhotoUtils.addPhotoPickerExtras(intent, outputUri); + ContactPhotoUtils.addCropExtras(intent, mPhotoPickSize); return intent; } @@ -297,7 +319,7 @@ public abstract class PhotoSelectionHandler implements OnClickListener { 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 abstract class PhotoSelectionHandler implements OnClickListener { 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 @@ public abstract class PhotoSelectionHandler implements OnClickListener { /** * 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 66b6f1c0b..f2031d351 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.ImmutableList; 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 @@ public class ContactEditorFragment extends Fragment implements 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"; @@ -222,7 +223,7 @@ public class ContactEditorFragment extends Fragment implements private Cursor mGroupMetaData; - private String mCurrentPhotoFile; + private Uri mCurrentPhotoUri; private Bundle mUpdatedPhotos = new Bundle(); private Context mContext; @@ -484,7 +485,7 @@ public class ContactEditorFragment extends Fragment implements 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); @@ -1680,7 +1681,7 @@ public class ContactEditorFragment extends Fragment implements } 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); @@ -1748,7 +1749,7 @@ public class ContactEditorFragment extends Fragment implements /** * 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) { @@ -1762,9 +1763,7 @@ public class ContactEditorFragment extends Fragment implements 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); } /** @@ -1797,11 +1796,12 @@ public class ContactEditorFragment extends Fragment implements 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) { } } } @@ -1912,11 +1912,11 @@ public class ContactEditorFragment extends Fragment implements } @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); } @@ -1977,15 +1977,16 @@ public class ContactEditorFragment extends Fragment implements } @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 b14b36cce..2b1c19a28 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 @@ public class ContactPhotoUtils { 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"); - dir.mkdirs(); - final File f = new File(dir, fileName); - return f.getAbsolutePath(); + public static Uri generateTempCroppedImageUri(Context context) { + return FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY, + new File(pathForTempPhoto(context, generateTempCroppedPhotoFileName()))); } - public static String pathForNewCameraPhoto(String fileName) { - final File dir = new File(NEW_PHOTO_DIR_PATH); + 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 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 @@ public class ContactPhotoUtils { } } - /** - * 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 @@ public class ContactPhotoUtils { 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; } } |